summaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 13:00:47 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 13:00:47 +0000
commit2cb7e0aaedad73b076ea18c6900b0e86c5760d79 (patch)
treeda68ca54bb79f4080079bf0828acda937593a4e1 /tools
parentInitial commit. (diff)
downloadsystemd-upstream.tar.xz
systemd-upstream.zip
Adding upstream version 247.3.upstream/247.3upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rwxr-xr-xtools/add-git-hook.sh12
-rwxr-xr-xtools/autosuspend-update.sh7
-rwxr-xr-xtools/catalog-report.py84
-rwxr-xr-xtools/check-api-docs.sh42
-rwxr-xr-xtools/check-compilation.sh4
-rwxr-xr-xtools/check-directives.sh36
-rwxr-xr-xtools/check-help.sh29
-rwxr-xr-xtools/check-includes.pl23
-rwxr-xr-xtools/choose-default-locale.sh11
-rw-r--r--tools/chromiumos/LICENSE27
-rw-r--r--tools/chromiumos/gen_autosuspend_rules.py340
-rwxr-xr-xtools/coverity.sh233
-rwxr-xr-xtools/find-build-dir.sh32
-rwxr-xr-xtools/find-double-newline.sh41
-rwxr-xr-xtools/find-tabs.sh41
-rw-r--r--tools/gdb-sd_dump_hashmaps.py77
-rwxr-xr-xtools/generate-gperfs.py24
-rwxr-xr-xtools/git-contrib.sh6
-rwxr-xr-xtools/hwdb-update.sh32
-rwxr-xr-xtools/make-autosuspend-rules.py24
-rwxr-xr-xtools/make-directive-index.py173
-rwxr-xr-xtools/make-man-index.py111
-rwxr-xr-xtools/meson-apply-m4.sh24
-rwxr-xr-xtools/meson-build.sh20
-rwxr-xr-xtools/meson-make-symlink.sh12
-rwxr-xr-xtools/meson-vcs-tag.sh22
-rwxr-xr-xtools/oss-fuzz.sh61
-rwxr-xr-xtools/syscall-names-update.sh6
-rwxr-xr-xtools/update-dbus-docs.py333
-rwxr-xr-xtools/update-man-rules.py86
-rwxr-xr-xtools/xml_helper.py20
31 files changed, 1993 insertions, 0 deletions
diff --git a/tools/add-git-hook.sh b/tools/add-git-hook.sh
new file mode 100755
index 0000000..5b1bf17
--- /dev/null
+++ b/tools/add-git-hook.sh
@@ -0,0 +1,12 @@
+#!/bin/sh
+set -eu
+
+cd "$MESON_SOURCE_ROOT"
+
+if [ ! -f .git/hooks/pre-commit.sample -o -f .git/hooks/pre-commit ]; then
+ exit 2 # not needed
+fi
+
+cp -p .git/hooks/pre-commit.sample .git/hooks/pre-commit
+chmod +x .git/hooks/pre-commit
+echo 'Activated pre-commit hook'
diff --git a/tools/autosuspend-update.sh b/tools/autosuspend-update.sh
new file mode 100755
index 0000000..a4f99eb
--- /dev/null
+++ b/tools/autosuspend-update.sh
@@ -0,0 +1,7 @@
+#!/bin/sh
+set -eu
+
+cd "$1"
+
+(curl -L 'https://chromium.googlesource.com/chromiumos/platform2/+/master/power_manager/udev/gen_autosuspend_rules.py?format=TEXT'; echo) \
+ | base64 -d > gen_autosuspend_rules.py
diff --git a/tools/catalog-report.py b/tools/catalog-report.py
new file mode 100755
index 0000000..ca1e13d
--- /dev/null
+++ b/tools/catalog-report.py
@@ -0,0 +1,84 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: MIT
+#
+# This file is distributed under the MIT license, see below.
+#
+# The MIT License (MIT)
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+"""
+Prints out journal entries with no or bad catalog explanations.
+"""
+
+import re
+from systemd import journal, id128
+
+j = journal.Reader()
+
+logged = set()
+pattern = re.compile('@[A-Z0-9_]+@')
+
+mids = {v:k for k,v in id128.__dict__.items()
+ if k.startswith('SD_MESSAGE')}
+
+freq = 1000
+
+def log_entry(x):
+ if 'CODE_FILE' in x:
+ # some of our code was using 'CODE_FUNCTION' instead of 'CODE_FUNC'
+ print('{}:{} {}'.format(x.get('CODE_FILE', '???'),
+ x.get('CODE_LINE', '???'),
+ x.get('CODE_FUNC', None) or x.get('CODE_FUNCTION', '???')))
+ print(' {}'.format(x.get('MESSAGE', 'no message!')))
+ for k, v in x.items():
+ if k.startswith('CODE_') or k in {'MESSAGE_ID', 'MESSAGE'}:
+ continue
+ print(' {}={}'.format(k, v))
+ print()
+
+for i, x in enumerate(j):
+ if i % freq == 0:
+ print(i, end='\r')
+
+ try:
+ mid = x['MESSAGE_ID']
+ except KeyError:
+ continue
+ name = mids.get(mid, 'unknown')
+
+ try:
+ desc = journal.get_catalog(mid)
+ except FileNotFoundError:
+ if mid in logged:
+ continue
+
+ print('{} {.hex}: no catalog entry'.format(name, mid))
+ log_entry(x)
+ logged.add(mid)
+ continue
+
+ fields = [field[1:-1] for field in pattern.findall(desc)]
+ for field in fields:
+ index = (mid, field)
+ if field in x or index in logged:
+ continue
+ print('{} {.hex}: no field {}'.format(name, mid, field))
+ log_entry(x)
+ logged.add(index)
diff --git a/tools/check-api-docs.sh b/tools/check-api-docs.sh
new file mode 100755
index 0000000..1094101
--- /dev/null
+++ b/tools/check-api-docs.sh
@@ -0,0 +1,42 @@
+#!/bin/sh
+set -eu
+
+sd_good=0
+sd_total=0
+udev_good=0
+udev_total=0
+
+deprecated="
+ -e sd_bus_try_close
+ -e sd_bus_process_priority
+ -e sd_bus_message_get_priority
+ -e sd_bus_message_set_priority
+ -e sd_seat_can_multi_session
+ -e sd_journal_open_container
+"
+
+for symbol in `nm -g --defined-only "$@" | grep " T " | cut -d" " -f3 | grep -wv $deprecated | sort -u` ; do
+ if test -f ${MESON_BUILD_ROOT}/man/$symbol.3 ; then
+ echo "✓ Symbol $symbol() is documented."
+ good=1
+ else
+ printf " \x1b[1;31mSymbol $symbol() lacks documentation.\x1b[0m\n"
+ good=0
+ fi
+
+ case $symbol in
+ sd_*)
+ ((sd_good+=good))
+ ((sd_total+=1))
+ ;;
+ udev_*)
+ ((udev_good+=good))
+ ((udev_total+=1))
+ ;;
+ *)
+ echo 'unknown symbol prefix'
+ exit 1
+ esac
+done
+
+echo "libsystemd: $sd_good/$sd_total libudev: $udev_good/$udev_total"
diff --git a/tools/check-compilation.sh b/tools/check-compilation.sh
new file mode 100755
index 0000000..ce39e16
--- /dev/null
+++ b/tools/check-compilation.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+set -eu
+
+"$@" '-' -o/dev/null </dev/null
diff --git a/tools/check-directives.sh b/tools/check-directives.sh
new file mode 100755
index 0000000..1a0bb09
--- /dev/null
+++ b/tools/check-directives.sh
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+set -e
+
+which perl &>/dev/null || exit 77
+
+function generate_directives() {
+ perl -aF'/[\s,]+/' -ne '
+ if (my ($s, $d) = ($F[0] =~ /^([^\s\.]+)\.([^\s\.]+)$/)) { $d{$s}{"$d="} = 1; }
+ END { while (my ($key, $value) = each %d) {
+ printf "[%s]\n%s\n", $key, join("\n", keys(%$value))
+ }}' "$1"
+}
+
+ret=0
+if ! diff \
+ <(generate_directives "$1"/src/network/networkd-network-gperf.gperf | sort) \
+ <(cat "$1"/test/fuzz/fuzz-network-parser/directives.network | sort); then
+ echo "Looks like test/fuzz/fuzz-network-parser/directives.network hasn't been updated"
+ ret=1
+fi
+
+if ! diff \
+ <(generate_directives "$1"/src/network/netdev/netdev-gperf.gperf | sort) \
+ <(cat "$1"/test/fuzz/fuzz-netdev-parser/directives.netdev | sort); then
+ echo "Looks like test/fuzz/fuzz-netdev-parser/directives.netdev hasn't been updated"
+ ret=1
+fi
+
+if ! diff \
+ <(generate_directives "$1"/src/udev/net/link-config-gperf.gperf | sort) \
+ <(cat "$1"/test/fuzz/fuzz-link-parser/directives.link | sort) ; then
+ echo "Looks like test/fuzz/fuzz-link-parser/directives.link hasn't been updated"
+ ret=1
+fi
+
+exit $ret
diff --git a/tools/check-help.sh b/tools/check-help.sh
new file mode 100755
index 0000000..efe7ed4
--- /dev/null
+++ b/tools/check-help.sh
@@ -0,0 +1,29 @@
+#!/bin/sh
+set -eu
+
+export SYSTEMD_LOG_LEVEL=info
+
+# output width
+if "$1" --help | grep -v 'default:' | grep -E -q '.{80}.'; then
+ echo "$(basename "$1") --help output is too wide:"
+ "$1" --help | awk 'length > 80' | grep -E --color=yes '.{80}'
+ exit 1
+fi
+
+# --help prints something. Also catches case where args are ignored.
+if ! "$1" --help | grep -q .; then
+ echo "$(basename "$1") --help output is empty."
+ exit 2
+fi
+
+# no --help output to stdout
+if "$1" --help 2>&1 1>/dev/null | grep .; then
+ echo "$(basename "$1") --help prints to stderr"
+ exit 3
+fi
+
+# error output to stderr
+if ! "$1" --no-such-parameter 2>&1 1>/dev/null | grep -q .; then
+ echo "$(basename "$1") with an unknown parameter does not print to stderr"
+ exit 4
+fi
diff --git a/tools/check-includes.pl b/tools/check-includes.pl
new file mode 100755
index 0000000..c8bfcba
--- /dev/null
+++ b/tools/check-includes.pl
@@ -0,0 +1,23 @@
+# SPDX-License-Identifier: CC0-1.0
+#!/usr/bin/env perl
+#
+# checkincludes: Find files included more than once in (other) files.
+
+foreach $file (@ARGV) {
+ open(FILE, $file) or die "Cannot open $file: $!.\n";
+
+ my %includedfiles = ();
+
+ while (<FILE>) {
+ if (m/^\s*#\s*include\s*[<"](\S*)[>"]/o) {
+ ++$includedfiles{$1};
+ }
+ }
+ foreach $filename (keys %includedfiles) {
+ if ($includedfiles{$filename} > 1) {
+ print "$file: $filename is included more than once.\n";
+ }
+ }
+
+ close(FILE);
+}
diff --git a/tools/choose-default-locale.sh b/tools/choose-default-locale.sh
new file mode 100755
index 0000000..da9768a
--- /dev/null
+++ b/tools/choose-default-locale.sh
@@ -0,0 +1,11 @@
+#!/bin/sh
+set -e
+
+# Fedora uses C.utf8 but Debian uses C.UTF-8
+if locale -a | grep -xq -E 'C\.(utf8|UTF-8)'; then
+ echo 'C.UTF-8'
+elif locale -a | grep -xqF 'en_US.utf8'; then
+ echo 'en_US.UTF-8'
+else
+ echo 'C'
+fi
diff --git a/tools/chromiumos/LICENSE b/tools/chromiumos/LICENSE
new file mode 100644
index 0000000..b9e779f
--- /dev/null
+++ b/tools/chromiumos/LICENSE
@@ -0,0 +1,27 @@
+// Copyright 2014 The Chromium OS Authors. All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/tools/chromiumos/gen_autosuspend_rules.py b/tools/chromiumos/gen_autosuspend_rules.py
new file mode 100644
index 0000000..8bb25a1
--- /dev/null
+++ b/tools/chromiumos/gen_autosuspend_rules.py
@@ -0,0 +1,340 @@
+#!/usr/bin/env python2
+# -*- coding: utf-8 -*-
+
+# Copyright 2017 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Autosuspend udev rule generator
+
+This script is executed at build time to generate udev rules. The
+resulting rules file is installed on the device, the script itself
+is not.
+"""
+
+from __future__ import print_function
+
+# List of USB devices (vendorid:productid) for which it is safe to enable
+# autosuspend.
+USB_IDS = []
+
+# Host Controllers and internal hubs
+USB_IDS += [
+ # Linux Host Controller (UHCI) (most older x86 boards)
+ '1d6b:0001',
+ # Linux Host Controller (EHCI) (all boards)
+ '1d6b:0002',
+ # Linux Host Controller (XHCI) (most newer boards)
+ '1d6b:0003',
+ # SMSC (Internal HSIC Hub) (most Exynos boards)
+ '0424:3503',
+ # Intel (Rate Matching Hub) (all x86 boards)
+ '05e3:0610',
+ # Intel (Internal Hub?) (peppy, falco)
+ '8087:0024',
+ # Genesys Logic (Internal Hub) (rambi)
+ '8087:8000',
+ # Microchip (Composite HID + CDC) (kefka)
+ '04d8:0b28',
+]
+
+# Webcams
+USB_IDS += [
+ # Chicony (zgb)
+ '04f2:b1d8',
+ # Chicony (mario)
+ '04f2:b262',
+ # Chicony (stout)
+ '04f2:b2fe',
+ # Chicony (butterfly)
+ '04f2:b35f',
+ # Chicony (rambi)
+ '04f2:b443',
+ # Chicony (glados)
+ '04f2:b552',
+ # LiteOn (spring)
+ '058f:b001',
+ # Foxlink? (butterfly)
+ '05c8:0351',
+ # Foxlink? (butterfly)
+ '05c8:0355',
+ # Cheng Uei? (falco)
+ '05c8:036e',
+ # SuYin (parrot)
+ '064e:d251',
+ # Realtek (falco)
+ '0bda:571c',
+ # IMC Networks (squawks)
+ '13d3:5657',
+ # Sunplus (parrot)
+ '1bcf:2c17',
+ # (C-13HDO10B39N) (alex)
+ '2232:1013',
+ # (C-10HDP11538N) (lumpy)
+ '2232:1017',
+ # (Namuga) (link)
+ '2232:1033',
+ # (C-03FFM12339N) (daisy)
+ '2232:1037',
+ # (C-10HDO13531N) (peach)
+ '2232:1056',
+ # (NCM-G102) (samus)
+ '2232:6001',
+ # Acer (stout)
+ '5986:0299',
+]
+
+# Bluetooth Host Controller
+USB_IDS += [
+ # Hon-hai (parrot)
+ '0489:e04e',
+ # Hon-hai (peppy)
+ '0489:e056',
+ # Hon-hai (Kahlee)
+ '0489:e09f',
+ # QCA6174A (delan)
+ '0489:e0a2',
+ # LiteOn (parrot)
+ '04ca:3006',
+ # LiteOn (aleena)
+ '04ca:3016',
+ # LiteOn (scarlet)
+ '04ca:301a',
+ # Realtek (blooglet)
+ '0bda:b00c',
+ # Atheros (stumpy, stout)
+ '0cf3:3004',
+ # Atheros (AR3011) (mario, alex, zgb)
+ '0cf3:3005',
+ # Atheros (stumyp)
+ '0cf3:3007',
+ # Atheros (butterfly)
+ '0cf3:311e',
+ # Atheros (scarlet)
+ '0cf3:e300',
+ # Marvell (rambi)
+ '1286:2046',
+ # Marvell (gru)
+ '1286:204e',
+ # Intel (rambi, samus)
+ '8087:07dc',
+ # Intel (strago, glados)
+ '8087:0a2a',
+ # Intel (octopus)
+ '8087:0aaa',
+ # Intel (hatch)
+ '8087:0026',
+ # Intel (atlas)
+ '8087:0025',
+]
+
+# WWAN (LTE)
+USB_IDS += [
+ # Huawei (ME936) (kip)
+ '12d1:15bb',
+ # Fibocom (L850-GL) (coral, nautilus, sarien)
+ '2cb7:0007',
+ # Fibocom (NL668, NL652)
+ '2cb7:01a0',
+]
+
+# Mass Storage
+USB_IDS += [
+ # Genesys (SD card reader) (lumpy, link, peppy)
+ '05e3:0727',
+ # Realtek (SD card reader) (mario, alex)
+ '0bda:0138',
+ # Realtek (SD card reader) (helios)
+ '0bda:0136',
+ # Realtek (SD card reader) (falco)
+ '0bda:0177',
+]
+
+# Security Key
+USB_IDS += [
+ # Yubico.com
+ '1050:0211',
+ # Yubico.com (HID firmware)
+ '1050:0200',
+ # Google Titan key
+ '18d1:5026',
+]
+
+# USB Audio devices
+USB_IDS += [
+ # Google USB-C to 3.5mm Digital Headphone Jack Adapter 'Mir'
+ '18d1:5025',
+ # Google USB-C to 3.5mm Digital Headphone Jack Adapter 'Mir' (HID only)
+ '18d1:5029',
+ # Google USB-C to 3.5mm Digital Headphone Jack Adapter 2018 'Condor'
+ '18d1:5034',
+ # Google Pixel USB-C Earbuds 'Blackbird'
+ '18d1:5033',
+ # Libratone Q Adapt In-Ear USB-C Earphones, Made for Google
+ '03eb:2433',
+ # Moshi USB-C to 3.5 mm Adapter/Charger, Made for Google
+ '282b:48f0',
+ # Moshi USB-C to 3.5 mm Adapter/Charger, Made for Google (HID only)
+ '282b:0026',
+ # AiAiAi TMA-2 C60 Cable, Made for Google
+ '0572:1a08',
+ # Apple USB-C to 3.5mm Headphone Jack Adapter
+ '05ac:110a',
+]
+
+# List of PCI devices (vendorid:deviceid) for which it is safe to enable
+# autosuspend.
+PCI_IDS = []
+
+# Intel
+PCI_IDS += [
+ # Host bridge
+ '8086:590c',
+ # i915
+ '8086:591e',
+ # proc_thermal
+ '8086:1903',
+ # SPT PCH xHCI controller
+ '8086:9d2f',
+ # CNP PCH xHCI controller
+ '8086:9ded',
+ # intel_pmc_core
+ '8086:9d21',
+ # i801_smbus
+ '8086:9d23',
+ # iwlwifi
+ '8086:095a',
+ # GMM
+ '8086:1911',
+ # Thermal
+ '8086:9d31',
+ # MME
+ '8086:9d3a',
+ # CrOS EC
+ '8086:9d4b',
+ # PCH SPI
+ '8086:9d24',
+ # SATA
+ '8086:02d3',
+ # RAM memory
+ '8086:02ef',
+ # ISA bridge
+ '8086:0284',
+ # Communication controller
+ '8086:02e0',
+ # Network controller
+ '8086:02f0',
+ # Serial bus controller
+ '8086:02a4',
+ # USB controller
+ '8086:02ed',
+ # Volteer xHCI controller
+ '8086:a0ed',
+ # Graphics
+ '8086:9b41',
+ # DSP
+ '8086:02f9',
+ # Host bridge
+ '8086:9b61',
+ # Host bridge
+ '8086:9b71',
+ # PCI Bridge
+ '8086:02b0',
+ # i915 (atlas)
+ '8086:591c',
+ # iwlwifi (atlas)
+ '8086:2526',
+ # i915 (kefka)
+ '8086:22b1',
+ # proc_thermal (kefka)
+ '8086:22dc',
+ # xchi_hdc (kefka)
+ '8086:22b5',
+ # snd_hda (kefka)
+ '8086:2284',
+ # pcieport (kefka)
+ '8086:22c8',
+ '8086:22cc',
+ # lpc_ich (kefka)
+ '8086:229c',
+ # iosf_mbi_pci (kefka)
+ '8086:2280',
+]
+
+# Samsung
+PCI_IDS += [
+ # NVMe KUS030205M-B001
+ '144d:a806',
+ # NVMe MZVLB256HAHQ
+ '144d:a808',
+]
+
+# Lite-on
+PCI_IDS += [
+ # 3C07110288
+ '14a4:9100',
+]
+
+# Seagate
+PCI_IDS += [
+ # ZP256CM30011
+ '7089:5012',
+]
+
+# Kingston
+PCI_IDS += [
+ # RBUSNS8154P3128GJ3
+ '2646:5008',
+]
+
+# Do not edit below this line. #################################################
+
+UDEV_RULE = """\
+ACTION!="add", GOTO="autosuspend_end"
+SUBSYSTEM!="i2c|pci|usb", GOTO="autosuspend_end"
+
+SUBSYSTEM=="i2c", GOTO="autosuspend_i2c"
+SUBSYSTEM=="pci", GOTO="autosuspend_pci"
+SUBSYSTEM=="usb", GOTO="autosuspend_usb"
+
+# I2C rules
+LABEL="autosuspend_i2c"
+ATTR{name}=="cyapa", ATTR{power/control}="on", GOTO="autosuspend_end"
+GOTO="autosuspend_end"
+
+# PCI rules
+LABEL="autosuspend_pci"
+%(pci_rules)s\
+GOTO="autosuspend_end"
+
+# USB rules
+LABEL="autosuspend_usb"
+%(usb_rules)s\
+GOTO="autosuspend_end"
+
+# Enable autosuspend
+LABEL="autosuspend_enable"
+TEST=="power/control", ATTR{power/control}="auto", GOTO="autosuspend_end"
+
+LABEL="autosuspend_end"
+"""
+
+
+def main():
+ pci_rules = ''
+ for dev_ids in PCI_IDS:
+ vendor, device = dev_ids.split(':')
+ pci_rules += ('ATTR{vendor}=="0x%s", ATTR{device}=="0x%s", '
+ 'GOTO="autosuspend_enable"\n' % (vendor, device))
+
+ usb_rules = ''
+ for dev_ids in USB_IDS:
+ vid, pid = dev_ids.split(':')
+ usb_rules += ('ATTR{idVendor}=="%s", ATTR{idProduct}=="%s", '
+ 'GOTO="autosuspend_enable"\n' % (vid, pid))
+
+ print(UDEV_RULE % {'pci_rules': pci_rules, 'usb_rules': usb_rules})
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/coverity.sh b/tools/coverity.sh
new file mode 100755
index 0000000..5d3b7e2
--- /dev/null
+++ b/tools/coverity.sh
@@ -0,0 +1,233 @@
+#!/usr/bin/env bash
+
+# The official unmodified version of the script can be found at
+# https://scan.coverity.com/scripts/travisci_build_coverity_scan.sh
+
+set -e
+
+# Declare build command
+COVERITY_SCAN_BUILD_COMMAND="ninja -C cov-build"
+
+# Environment check
+# Use default values if not set
+SCAN_URL=${SCAN_URL:="https://scan.coverity.com"}
+TOOL_BASE=${TOOL_BASE:="/tmp/coverity-scan-analysis"}
+UPLOAD_URL=${UPLOAD_URL:="https://scan.coverity.com/builds"}
+
+# These must be set by environment
+echo -e "\033[33;1mNote: COVERITY_SCAN_PROJECT_NAME and COVERITY_SCAN_TOKEN are available on Project Settings page on scan.coverity.com\033[0m"
+[ -z "$COVERITY_SCAN_PROJECT_NAME" ] && echo "ERROR: COVERITY_SCAN_PROJECT_NAME must be set" && exit 1
+[ -z "$COVERITY_SCAN_NOTIFICATION_EMAIL" ] && echo "ERROR: COVERITY_SCAN_NOTIFICATION_EMAIL must be set" && exit 1
+[ -z "$COVERITY_SCAN_BRANCH_PATTERN" ] && echo "ERROR: COVERITY_SCAN_BRANCH_PATTERN must be set" && exit 1
+[ -z "$COVERITY_SCAN_BUILD_COMMAND" ] && echo "ERROR: COVERITY_SCAN_BUILD_COMMAND must be set" && exit 1
+[ -z "$COVERITY_SCAN_TOKEN" ] && echo "ERROR: COVERITY_SCAN_TOKEN must be set" && exit 1
+
+# Do not run on pull requests
+if [ "${TRAVIS_PULL_REQUEST}" = "true" ]; then
+ echo -e "\033[33;1mINFO: Skipping Coverity Analysis: branch is a pull request.\033[0m"
+ exit 0
+fi
+
+# Verify this branch should run
+if [[ "${TRAVIS_BRANCH^^}" =~ "${COVERITY_SCAN_BRANCH_PATTERN^^}" ]]; then
+ echo -e "\033[33;1mCoverity Scan configured to run on branch ${TRAVIS_BRANCH}\033[0m"
+else
+ echo -e "\033[33;1mCoverity Scan NOT configured to run on branch ${TRAVIS_BRANCH}\033[0m"
+ exit 1
+fi
+
+# Verify upload is permitted
+AUTH_RES=`curl -s --form project="$COVERITY_SCAN_PROJECT_NAME" --form token="$COVERITY_SCAN_TOKEN" $SCAN_URL/api/upload_permitted`
+if [ "$AUTH_RES" = "Access denied" ]; then
+ echo -e "\033[33;1mCoverity Scan API access denied. Check COVERITY_SCAN_PROJECT_NAME and COVERITY_SCAN_TOKEN.\033[0m"
+ exit 1
+else
+ AUTH=`echo $AUTH_RES | jq .upload_permitted`
+ if [ "$AUTH" = "true" ]; then
+ echo -e "\033[33;1mCoverity Scan analysis authorized per quota.\033[0m"
+ else
+ WHEN=`echo $AUTH_RES | jq .next_upload_permitted_at`
+ echo -e "\033[33;1mCoverity Scan analysis NOT authorized until $WHEN.\033[0m"
+ exit 1
+ fi
+fi
+
+TOOL_DIR=`find $TOOL_BASE -type d -name 'cov-analysis*'`
+export PATH="$TOOL_DIR/bin:$PATH"
+
+# Disable CCACHE for cov-build to compilation units correctly
+export CCACHE_DISABLE=1
+
+# FUNCTION DEFINITIONS
+# --------------------
+_help()
+{
+ # displays help and exits
+ cat <<-EOF
+ USAGE: $0 [CMD] [OPTIONS]
+
+ CMD
+ build Issue Coverity build
+ upload Upload coverity archive for analysis
+ Note: By default, archive is created from default results directory.
+ To provide custom archive or results directory, see --result-dir
+ and --tar options below.
+
+ OPTIONS
+ -h,--help Display this menu and exits
+
+ Applicable to build command
+ ---------------------------
+ -o,--out-dir Specify Coverity intermediate directory (defaults to 'cov-int')
+ -t,--tar bool, archive the output to .tgz file (defaults to false)
+
+ Applicable to upload command
+ ----------------------------
+ -d, --result-dir Specify result directory if different from default ('cov-int')
+ -t, --tar ARCHIVE Use custom .tgz archive instead of intermediate directory or pre-archived .tgz
+ (by default 'analysis-result.tgz'
+ EOF
+ return;
+}
+
+_pack()
+{
+ RESULTS_ARCHIVE=${RESULTS_ARCHIVE:-'analysis-results.tgz'}
+
+ echo -e "\033[33;1mTarring Coverity Scan Analysis results...\033[0m"
+ tar czf $RESULTS_ARCHIVE $RESULTS_DIR
+ SHA=`git rev-parse --short HEAD`
+
+ PACKED=true
+}
+
+
+_build()
+{
+ echo -e "\033[33;1mRunning Coverity Scan Analysis Tool...\033[0m"
+ local _cov_build_options=""
+ #local _cov_build_options="--return-emit-failures 8 --parse-error-threshold 85"
+ eval "${COVERITY_SCAN_BUILD_COMMAND_PREPEND}"
+ COVERITY_UNSUPPORTED=1 cov-build --dir $RESULTS_DIR $_cov_build_options sh -c "$COVERITY_SCAN_BUILD_COMMAND"
+ cov-import-scm --dir $RESULTS_DIR --scm git --log $RESULTS_DIR/scm_log.txt
+
+ if [ $? != 0 ]; then
+ echo -e "\033[33;1mCoverity Scan Build failed: $TEXT.\033[0m"
+ return 1
+ fi
+
+ [ -z $TAR ] || [ $TAR = false ] && return 0
+
+ if [ "$TAR" = true ]; then
+ _pack
+ fi
+}
+
+
+_upload()
+{
+ # pack results
+ [ -z $PACKED ] || [ $PACKED = false ] && _pack
+
+ # Upload results
+ echo -e "\033[33;1mUploading Coverity Scan Analysis results...\033[0m"
+ response=$(curl \
+ --silent --write-out "\n%{http_code}\n" \
+ --form project=$COVERITY_SCAN_PROJECT_NAME \
+ --form token=$COVERITY_SCAN_TOKEN \
+ --form email=$COVERITY_SCAN_NOTIFICATION_EMAIL \
+ --form file=@$RESULTS_ARCHIVE \
+ --form version=$SHA \
+ --form description="Travis CI build" \
+ $UPLOAD_URL)
+ printf "\033[33;1mThe response is\033[0m\n%s\n" "$response"
+ status_code=$(echo "$response" | sed -n '$p')
+ # Coverity Scan used to respond with 201 on successfully receiving analysis results.
+ # Now for some reason it sends 200 and may change back in the foreseeable future.
+ # See https://github.com/pmem/pmdk/commit/7b103fd2dd54b2e5974f71fb65c81ab3713c12c5
+ if [ "$status_code" != "200" ]; then
+ TEXT=$(echo "$response" | sed '$d')
+ echo -e "\033[33;1mCoverity Scan upload failed: $TEXT.\033[0m"
+ exit 1
+ fi
+
+ echo -e "\n\033[33;1mCoverity Scan Analysis completed successfully.\033[0m"
+ exit 0
+}
+
+# PARSE COMMAND LINE OPTIONS
+# --------------------------
+
+case $1 in
+ -h|--help)
+ _help
+ exit 0
+ ;;
+ build)
+ CMD='build'
+ TEMP=`getopt -o ho:t --long help,out-dir:,tar -n '$0' -- "$@"`
+ _ec=$?
+ [[ $_ec -gt 0 ]] && _help && exit $_ec
+ shift
+ ;;
+ upload)
+ CMD='upload'
+ TEMP=`getopt -o hd:t: --long help,result-dir:tar: -n '$0' -- "$@"`
+ _ec=$?
+ [[ $_ec -gt 0 ]] && _help && exit $_ec
+ shift
+ ;;
+ *)
+ _help && exit 1 ;;
+esac
+
+RESULTS_DIR='cov-int'
+
+eval set -- "$TEMP"
+if [ $? != 0 ] ; then exit 1 ; fi
+
+# extract options and their arguments into variables.
+if [[ $CMD == 'build' ]]; then
+ TAR=false
+ while true ; do
+ case $1 in
+ -h|--help)
+ _help
+ exit 0
+ ;;
+ -o|--out-dir)
+ RESULTS_DIR="$2"
+ shift 2
+ ;;
+ -t|--tar)
+ TAR=true
+ shift
+ ;;
+ --) _build; shift ; break ;;
+ *) echo "Internal error" ; _help && exit 6 ;;
+ esac
+ done
+
+elif [[ $CMD == 'upload' ]]; then
+ while true ; do
+ case $1 in
+ -h|--help)
+ _help
+ exit 0
+ ;;
+ -d|--result-dir)
+ CHANGE_DEFAULT_DIR=true
+ RESULTS_DIR="$2"
+ shift 2
+ ;;
+ -t|--tar)
+ RESULTS_ARCHIVE="$2"
+ [ -z $CHANGE_DEFAULT_DIR ] || [ $CHANGE_DEFAULT_DIR = false ] && PACKED=true
+ shift 2
+ ;;
+ --) _upload; shift ; break ;;
+ *) echo "Internal error" ; _help && exit 6 ;;
+ esac
+ done
+
+fi
diff --git a/tools/find-build-dir.sh b/tools/find-build-dir.sh
new file mode 100755
index 0000000..fb8a1c1
--- /dev/null
+++ b/tools/find-build-dir.sh
@@ -0,0 +1,32 @@
+#!/bin/sh
+set -e
+
+# Try to guess the build directory:
+# we look for subdirectories of the parent directory that look like ninja build dirs.
+
+if [ -n "$BUILD_DIR" ]; then
+ echo "$(realpath "$BUILD_DIR")"
+ exit 0
+fi
+
+root="$(dirname "$(realpath "$0")")"
+
+found=
+for i in "$root"/../*/build.ninja; do
+ c="$(dirname $i)"
+ [ -d "$c" ] || continue
+ [ "$(basename "$c")" != mkosi.builddir ] || continue
+
+ if [ -n "$found" ]; then
+ echo 'Found multiple candidates, specify build directory with $BUILD_DIR' >&2
+ exit 2
+ fi
+ found="$c"
+done
+
+if [ -z "$found" ]; then
+ echo 'Specify build directory with $BUILD_DIR' >&2
+ exit 1
+fi
+
+echo "$(realpath $found)"
diff --git a/tools/find-double-newline.sh b/tools/find-double-newline.sh
new file mode 100755
index 0000000..7ea6de8
--- /dev/null
+++ b/tools/find-double-newline.sh
@@ -0,0 +1,41 @@
+#!/bin/sh
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+TOP=`git rev-parse --show-toplevel`
+
+case "$1" in
+ recdiff)
+ if [ "$2" = "" ] ; then
+ DIR="$TOP"
+ else
+ DIR="$2"
+ fi
+
+ find $DIR -type f \( -name '*.[ch]' -o -name '*.xml' \) -exec $0 diff \{\} \;
+ ;;
+
+ recpatch)
+ if [ "$2" = "" ] ; then
+ DIR="$TOP"
+ else
+ DIR="$2"
+ fi
+
+ find $DIR -type f \( -name '*.[ch]' -o -name '*.xml' \) -exec $0 patch \{\} \;
+ ;;
+
+ diff)
+ T=`mktemp`
+ sed '/^$/N;/^\n$/D' < "$2" > "$T"
+ diff -u "$2" "$T"
+ rm -f "$T"
+ ;;
+
+ patch)
+ sed -i '/^$/N;/^\n$/D' "$2"
+ ;;
+
+ *)
+ echo "Expected recdiff|recpatch|diff|patch as verb." >&2
+ ;;
+esac
diff --git a/tools/find-tabs.sh b/tools/find-tabs.sh
new file mode 100755
index 0000000..54d9229
--- /dev/null
+++ b/tools/find-tabs.sh
@@ -0,0 +1,41 @@
+#!/bin/sh
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+TOP=`git rev-parse --show-toplevel`
+
+case "$1" in
+ recdiff)
+ if [ "$2" = "" ] ; then
+ DIR="$TOP"
+ else
+ DIR="$2"
+ fi
+
+ find $DIR -type f \( -name '*.[ch]' -o -name '*.xml' \) -exec $0 diff \{\} \;
+ ;;
+
+ recpatch)
+ if [ "$2" = "" ] ; then
+ DIR="$TOP"
+ else
+ DIR="$2"
+ fi
+
+ find $DIR -type f \( -name '*.[ch]' -o -name '*.xml' \) -exec $0 patch \{\} \;
+ ;;
+
+ diff)
+ T=`mktemp`
+ sed 's/\t/ /g' < "$2" > "$T"
+ diff -u "$2" "$T"
+ rm -f "$T"
+ ;;
+
+ patch)
+ sed -i 's/\t/ /g' "$2"
+ ;;
+
+ *)
+ echo "Expected recdiff|recpatch|diff|patch as verb." >&2
+ ;;
+esac
diff --git a/tools/gdb-sd_dump_hashmaps.py b/tools/gdb-sd_dump_hashmaps.py
new file mode 100644
index 0000000..d2388b7
--- /dev/null
+++ b/tools/gdb-sd_dump_hashmaps.py
@@ -0,0 +1,77 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+import gdb
+
+class sd_dump_hashmaps(gdb.Command):
+ "dump systemd's hashmaps"
+
+ def __init__(self):
+ super().__init__("sd_dump_hashmaps", gdb.COMMAND_DATA, gdb.COMPLETE_NONE)
+
+ def invoke(self, arg, from_tty):
+ d = gdb.parse_and_eval("hashmap_debug_list")
+ hashmap_type_info = gdb.parse_and_eval("hashmap_type_info")
+ uchar_t = gdb.lookup_type("unsigned char")
+ ulong_t = gdb.lookup_type("unsigned long")
+ debug_offset = gdb.parse_and_eval("(unsigned long)&((HashmapBase*)0)->debug")
+
+ print("type, hash, indirect, entries, max_entries, buckets, creator")
+ while d:
+ h = gdb.parse_and_eval(f"(HashmapBase*)((char*){int(d.cast(ulong_t))} - {debug_offset})")
+
+ if h["has_indirect"]:
+ storage_ptr = h["indirect"]["storage"].cast(uchar_t.pointer())
+ n_entries = h["indirect"]["n_entries"]
+ n_buckets = h["indirect"]["n_buckets"]
+ else:
+ storage_ptr = h["direct"]["storage"].cast(uchar_t.pointer())
+ n_entries = h["n_direct_entries"]
+ n_buckets = hashmap_type_info[h["type"]]["n_direct_buckets"]
+
+ t = ["plain", "ordered", "set"][int(h["type"])]
+
+ print(f'{t}, {h["hash_ops"]}, {bool(h["has_indirect"])}, {n_entries}, {d["max_entries"]}, {n_buckets}, {d["func"].string()}, {d["file"].string()}:{d["line"]}')
+
+ if arg != "" and n_entries > 0:
+ dib_raw_addr = storage_ptr + hashmap_type_info[h["type"]]["entry_size"] * n_buckets
+
+ histogram = {}
+ for i in range(0, n_buckets):
+ dib = int(dib_raw_addr[i])
+ histogram[dib] = histogram.get(dib, 0) + 1
+
+ for dib in sorted(histogram):
+ if dib != 255:
+ print(f"{dib:>3} {histogram[dib]:>8} {float(histogram[dib]/n_entries):.0%} of entries")
+ else:
+ print(f"{dib:>3} {histogram[dib]:>8} {float(histogram[dib]/n_buckets):.0%} of slots")
+ s = sum(dib*count for (dib, count) in histogram.items() if dib != 255) / n_entries
+ print(f"mean DIB of entries: {s}")
+
+ blocks = []
+ current_len = 1
+ prev = int(dib_raw_addr[0])
+ for i in range(1, n_buckets):
+ dib = int(dib_raw_addr[i])
+ if (dib == 255) != (prev == 255):
+ if prev != 255:
+ blocks += [[i, current_len]]
+ current_len = 1
+ else:
+ current_len += 1
+
+ prev = dib
+ if prev != 255:
+ blocks += [[i, current_len]]
+ # a block may be wrapped around
+ if len(blocks) > 1 and blocks[0][0] == blocks[0][1] and blocks[-1][0] == n_buckets - 1:
+ blocks[0][1] += blocks[-1][1]
+ blocks = blocks[0:-1]
+ print("max block: {}".format(max(blocks, key=lambda a: a[1])))
+ print("sum block lens: {}".format(sum(b[1] for b in blocks)))
+ print("mean block len: {}".format(sum(b[1] for b in blocks) / len(blocks)))
+
+ d = d["debug_list_next"]
+
+sd_dump_hashmaps()
diff --git a/tools/generate-gperfs.py b/tools/generate-gperfs.py
new file mode 100755
index 0000000..d240b2c
--- /dev/null
+++ b/tools/generate-gperfs.py
@@ -0,0 +1,24 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+"""
+Generate %-from-name.gperf from %-list.txt
+"""
+
+import sys
+
+name, prefix, input = sys.argv[1:]
+
+print("""\
+%{
+#if __GNUC__ >= 7
+_Pragma("GCC diagnostic ignored \\"-Wimplicit-fallthrough\\"")
+#endif
+%}""")
+print("""\
+struct {}_name {{ const char* name; int id; }};
+%null-strings
+%%""".format(name))
+
+for line in open(input):
+ print("{0}, {1}{0}".format(line.rstrip(), prefix))
diff --git a/tools/git-contrib.sh b/tools/git-contrib.sh
new file mode 100755
index 0000000..f6fccd6
--- /dev/null
+++ b/tools/git-contrib.sh
@@ -0,0 +1,6 @@
+#!/bin/sh
+set -eu
+
+git shortlog -s `git describe --abbrev=0 --match 'v[0-9][0-9][0-9]'`.. | \
+ awk '{ $1=""; print $0 "," }' | \
+ sort -u
diff --git a/tools/hwdb-update.sh b/tools/hwdb-update.sh
new file mode 100755
index 0000000..39efd75
--- /dev/null
+++ b/tools/hwdb-update.sh
@@ -0,0 +1,32 @@
+#!/bin/sh
+set -eu
+
+cd "$1"
+
+unset permissive
+if [ "${2:-}" = "-p" ]; then
+ permissive=1
+ shift
+else
+ permissive=0
+fi
+
+if [ "${2:-}" != "-n" ]; then (
+ [ -z "$permissive" ] || set +e
+ set -x
+
+ curl -L -o usb.ids 'http://www.linux-usb.org/usb.ids'
+ curl -L -o pci.ids 'http://pci-ids.ucw.cz/v2.2/pci.ids'
+ curl -L -o ma-large.txt 'http://standards-oui.ieee.org/oui/oui.txt'
+ curl -L -o ma-medium.txt 'http://standards-oui.ieee.org/oui28/mam.txt'
+ curl -L -o ma-small.txt 'http://standards-oui.ieee.org/oui36/oui36.txt'
+ curl -L -o pnp_id_registry.html 'https://uefi.org/uefi-pnp-export'
+ curl -L -o acpi_id_registry.html 'https://uefi.org/uefi-acpi-export'
+) fi
+
+set -x
+./acpi-update.py >20-acpi-vendor.hwdb.base
+patch -p0 -o- 20-acpi-vendor.hwdb.base <20-acpi-vendor.hwdb.patch >20-acpi-vendor.hwdb
+! diff -u 20-acpi-vendor.hwdb.base 20-acpi-vendor.hwdb >20-acpi-vendor.hwdb.patch
+
+./ids_parser.py
diff --git a/tools/make-autosuspend-rules.py b/tools/make-autosuspend-rules.py
new file mode 100755
index 0000000..633b771
--- /dev/null
+++ b/tools/make-autosuspend-rules.py
@@ -0,0 +1,24 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+# Generate autosuspend rules for devices that have been tested to work properly
+# with autosuspend by the Chromium OS team. Based on
+# https://chromium.googlesource.com/chromiumos/platform2/+/master/power_manager/udev/gen_autosuspend_rules.py
+
+import chromiumos.gen_autosuspend_rules
+
+print('# pci:v<00VENDOR>d<00DEVICE> (8 uppercase hexadecimal digits twice)')
+for entry in chromiumos.gen_autosuspend_rules.PCI_IDS:
+ vendor, device = entry.split(':')
+ vendor = int(vendor, 16)
+ device = int(device, 16)
+ print('pci:v{:08X}d{:08X}*'.format(vendor, device))
+
+print('# usb:v<VEND>p<PROD> (4 uppercase hexadecimal digits twice)')
+for entry in chromiumos.gen_autosuspend_rules.USB_IDS:
+ vendor, product = entry.split(':')
+ vendor = int(vendor, 16)
+ product = int(product, 16)
+ print('usb:v{:04X}p{:04X}*'.format(vendor, product))
+
+print(' ID_AUTOSUSPEND=1')
diff --git a/tools/make-directive-index.py b/tools/make-directive-index.py
new file mode 100755
index 0000000..bbdc557
--- /dev/null
+++ b/tools/make-directive-index.py
@@ -0,0 +1,173 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+import sys
+import collections
+import re
+from xml_helper import xml_parse, xml_print, tree
+from copy import deepcopy
+
+COLOPHON = '''\
+This index contains {count} entries in {sections} sections,
+referring to {pages} individual manual pages.
+'''
+
+def _extract_directives(directive_groups, formatting, page):
+ t = xml_parse(page)
+ section = t.find('./refmeta/manvolnum').text
+ pagename = t.find('./refmeta/refentrytitle').text
+
+ storopt = directive_groups['options']
+ for variablelist in t.iterfind('.//variablelist'):
+ klass = variablelist.attrib.get('class')
+ searchpath = variablelist.attrib.get('xpath','./varlistentry/term/varname')
+ storvar = directive_groups[klass or 'miscellaneous']
+ # <option>s go in OPTIONS, unless class is specified
+ for xpath, stor in ((searchpath, storvar),
+ ('./varlistentry/term/option',
+ storvar if klass else storopt)):
+ for name in variablelist.iterfind(xpath):
+ text = re.sub(r'([= ]).*', r'\1', name.text).rstrip()
+ if text.startswith('-'):
+ # for options, merge options with and without mandatory arg
+ text = text.partition('=')[0]
+ stor[text].append((pagename, section))
+ if text not in formatting:
+ # use element as formatted display
+ if name.text[-1] in "= '":
+ name.clear()
+ else:
+ name.tail = ''
+ name.text = text
+ formatting[text] = name
+ extra = variablelist.attrib.get('extra-ref')
+ if extra:
+ stor[extra].append((pagename, section))
+ if extra not in formatting:
+ elt = tree.Element("varname")
+ elt.text= extra
+ formatting[extra] = elt
+
+ storfile = directive_groups['filenames']
+ for xpath, absolute_only in (('.//refsynopsisdiv//filename', False),
+ ('.//refsynopsisdiv//command', False),
+ ('.//filename', True)):
+ for name in t.iterfind(xpath):
+ if absolute_only and not (name.text and name.text.startswith('/')):
+ continue
+ if name.attrib.get('index') == 'false':
+ continue
+ name.tail = ''
+ if name.text:
+ if name.text.endswith('*'):
+ name.text = name.text[:-1]
+ if not name.text.startswith('.'):
+ text = name.text.partition(' ')[0]
+ if text != name.text:
+ name.clear()
+ name.text = text
+ if text.endswith('/'):
+ text = text[:-1]
+ storfile[text].append((pagename, section))
+ if text not in formatting:
+ # use element as formatted display
+ formatting[text] = name
+ else:
+ text = ' '.join(name.itertext())
+ storfile[text].append((pagename, section))
+ formatting[text] = name
+
+ storfile = directive_groups['constants']
+ for name in t.iterfind('.//constant'):
+ if name.attrib.get('index') == 'false':
+ continue
+ name.tail = ''
+ if name.text.startswith('('): # a cast, strip it
+ name.text = name.text.partition(' ')[2]
+ storfile[name.text].append((pagename, section))
+ formatting[name.text] = name
+
+ storfile = directive_groups['specifiers']
+ for name in t.iterfind(".//table[@class='specifiers']//entry/literal"):
+ if name.text[0] != '%' or name.getparent().text is not None:
+ continue
+ if name.attrib.get('index') == 'false':
+ continue
+ storfile[name.text].append((pagename, section))
+ formatting[name.text] = name
+ for name in t.iterfind(".//literal[@class='specifiers']"):
+ storfile[name.text].append((pagename, section))
+ formatting[name.text] = name
+
+def _make_section(template, name, directives, formatting):
+ varlist = template.find(".//*[@id='{}']".format(name))
+ for varname, manpages in sorted(directives.items()):
+ entry = tree.SubElement(varlist, 'varlistentry')
+ term = tree.SubElement(entry, 'term')
+ display = deepcopy(formatting[varname])
+ term.append(display)
+
+ para = tree.SubElement(tree.SubElement(entry, 'listitem'), 'para')
+
+ b = None
+ for manpage, manvolume in sorted(set(manpages)):
+ if b is not None:
+ b.tail = ', '
+ b = tree.SubElement(para, 'citerefentry')
+ c = tree.SubElement(b, 'refentrytitle')
+ c.text = manpage
+ c.attrib['target'] = varname
+ d = tree.SubElement(b, 'manvolnum')
+ d.text = manvolume
+ entry.tail = '\n\n'
+
+def _make_colophon(template, groups):
+ count = 0
+ pages = set()
+ for group in groups:
+ count += len(group)
+ for pagelist in group.values():
+ pages |= set(pagelist)
+
+ para = template.find(".//para[@id='colophon']")
+ para.text = COLOPHON.format(count=count,
+ sections=len(groups),
+ pages=len(pages))
+
+def _make_page(template, directive_groups, formatting):
+ """Create an XML tree from directive_groups.
+
+ directive_groups = {
+ 'class': {'variable': [('manpage', 'manvolume'), ...],
+ 'variable2': ...},
+ ...
+ }
+ """
+ for name, directives in directive_groups.items():
+ _make_section(template, name, directives, formatting)
+
+ _make_colophon(template, directive_groups.values())
+
+ return template
+
+def make_page(template_path, xml_files):
+ "Extract directives from xml_files and return XML index tree."
+ template = xml_parse(template_path)
+ names = [vl.get('id') for vl in template.iterfind('.//variablelist')]
+ directive_groups = {name:collections.defaultdict(list)
+ for name in names}
+ formatting = {}
+ for page in xml_files:
+ try:
+ _extract_directives(directive_groups, formatting, page)
+ except Exception:
+ raise ValueError("failed to process " + page)
+
+ return _make_page(template, directive_groups, formatting)
+
+if __name__ == '__main__':
+ with open(sys.argv[1], 'wb') as f:
+ template_path = sys.argv[2]
+ xml_files = sys.argv[3:]
+ xml = make_page(template_path, xml_files)
+ f.write(xml_print(xml))
diff --git a/tools/make-man-index.py b/tools/make-man-index.py
new file mode 100755
index 0000000..bae36fb
--- /dev/null
+++ b/tools/make-man-index.py
@@ -0,0 +1,111 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+import collections
+import sys
+import re
+from xml_helper import xml_parse, xml_print, tree
+
+MDASH = ' — ' if sys.version_info.major >= 3 else ' -- '
+
+TEMPLATE = '''\
+<refentry id="systemd.index">
+
+ <refentryinfo>
+ <title>systemd.index</title>
+ <productname>systemd</productname>
+ </refentryinfo>
+
+ <refmeta>
+ <refentrytitle>systemd.index</refentrytitle>
+ <manvolnum>7</manvolnum>
+ </refmeta>
+
+ <refnamediv>
+ <refname>systemd.index</refname>
+ <refpurpose>List all manpages from the systemd project</refpurpose>
+ </refnamediv>
+</refentry>
+'''
+
+SUMMARY = '''\
+ <refsect1>
+ <title>See Also</title>
+ <para>
+ <citerefentry><refentrytitle>systemd.directives</refentrytitle><manvolnum>7</manvolnum></citerefentry>
+ </para>
+
+ <para id='counts' />
+ </refsect1>
+'''
+
+COUNTS = '\
+This index contains {count} entries, referring to {pages} individual manual pages.'
+
+
+def check_id(page, t):
+ id = t.getroot().get('id')
+ if not re.search('/' + id + '[.]', page):
+ raise ValueError("id='{}' is not the same as page name '{}'".format(id, page))
+
+def make_index(pages):
+ index = collections.defaultdict(list)
+ for p in pages:
+ t = xml_parse(p)
+ check_id(p, t)
+ section = t.find('./refmeta/manvolnum').text
+ refname = t.find('./refnamediv/refname').text
+ purpose_text = ' '.join(t.find('./refnamediv/refpurpose').itertext())
+ purpose = ' '.join(purpose_text.split())
+ for f in t.findall('./refnamediv/refname'):
+ infos = (f.text, section, purpose, refname)
+ index[f.text[0].upper()].append(infos)
+ return index
+
+def add_letter(template, letter, pages):
+ refsect1 = tree.SubElement(template, 'refsect1')
+ title = tree.SubElement(refsect1, 'title')
+ title.text = letter
+ para = tree.SubElement(refsect1, 'para')
+ for info in sorted(pages, key=lambda info: str.lower(info[0])):
+ refname, section, purpose, realname = info
+
+ b = tree.SubElement(para, 'citerefentry')
+ c = tree.SubElement(b, 'refentrytitle')
+ c.text = refname
+ d = tree.SubElement(b, 'manvolnum')
+ d.text = section
+
+ b.tail = MDASH + purpose # + ' (' + p + ')'
+
+ tree.SubElement(para, 'sbr')
+
+def add_summary(template, indexpages):
+ count = 0
+ pages = set()
+ for group in indexpages:
+ count += len(group)
+ for info in group:
+ refname, section, purpose, realname = info
+ pages.add((realname, section))
+
+ refsect1 = tree.fromstring(SUMMARY)
+ template.append(refsect1)
+
+ para = template.find(".//para[@id='counts']")
+ para.text = COUNTS.format(count=count, pages=len(pages))
+
+def make_page(*xml_files):
+ template = tree.fromstring(TEMPLATE)
+ index = make_index(xml_files)
+
+ for letter in sorted(index):
+ add_letter(template, letter, index[letter])
+
+ add_summary(template, index.values())
+
+ return template
+
+if __name__ == '__main__':
+ with open(sys.argv[1], 'wb') as f:
+ f.write(xml_print(make_page(*sys.argv[2:])))
diff --git a/tools/meson-apply-m4.sh b/tools/meson-apply-m4.sh
new file mode 100755
index 0000000..5fad8cd
--- /dev/null
+++ b/tools/meson-apply-m4.sh
@@ -0,0 +1,24 @@
+#!/bin/sh
+set -eu
+
+CONFIG=$1
+TARGET=$2
+
+if [ $# -ne 2 ]; then
+ echo 'Invalid number of arguments.'
+ exit 1
+fi
+
+if [ ! -f $CONFIG ]; then
+ echo "$CONFIG not found."
+ exit 2
+fi
+
+if [ ! -f $TARGET ]; then
+ echo "$TARGET not found."
+ exit 3
+fi
+
+DEFINES=$(awk '$1 == "#define" && $3 == "1" { printf "-D%s ", $2 }' $CONFIG)
+
+m4 -P $DEFINES $TARGET
diff --git a/tools/meson-build.sh b/tools/meson-build.sh
new file mode 100755
index 0000000..dea5541
--- /dev/null
+++ b/tools/meson-build.sh
@@ -0,0 +1,20 @@
+#!/bin/sh
+set -eux
+
+src="$1"
+dst="$2"
+target="$3"
+options="$4"
+CC="$5"
+CXX="$6"
+
+[ -f "$dst/ninja.build" ] || CC="$CC" CXX="$CXX" meson "$src" "$dst" $options
+
+# Locate ninja binary, on CentOS 7 it is called ninja-build, so
+# use that name if available.
+ninja=ninja
+if which ninja-build >/dev/null 2>&1 ; then
+ ninja=ninja-build
+fi
+
+"$ninja" -C "$dst" "$target"
diff --git a/tools/meson-make-symlink.sh b/tools/meson-make-symlink.sh
new file mode 100755
index 0000000..cdd5214
--- /dev/null
+++ b/tools/meson-make-symlink.sh
@@ -0,0 +1,12 @@
+#!/bin/sh
+set -eu
+
+# this is needed mostly because $DESTDIR is provided as a variable,
+# and we need to create the target directory...
+
+mkdir -vp "$(dirname "${DESTDIR:-}$2")"
+if [ "$(dirname $1)" = . -o "$(dirname $1)" = .. ]; then
+ ln -vfs -T -- "$1" "${DESTDIR:-}$2"
+else
+ ln -vfs -T --relative -- "${DESTDIR:-}$1" "${DESTDIR:-}$2"
+fi
diff --git a/tools/meson-vcs-tag.sh b/tools/meson-vcs-tag.sh
new file mode 100755
index 0000000..1c3814d
--- /dev/null
+++ b/tools/meson-vcs-tag.sh
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+
+set -eu
+set -o pipefail
+
+dir="$1"
+tag="$2"
+fallback="$3"
+
+if [ -n "$tag" ]; then
+ echo "$tag"
+ exit 0
+fi
+
+# Apparently git describe has a bug where it always considers the work-tree
+# dirty when invoked with --git-dir (even though 'git status' is happy). Work
+# around this issue by cd-ing to the source directory.
+cd "$dir"
+# Check that we have either .git/ (a normal clone) or a .git file (a work-tree)
+# and that we don't get confused if a tarball is extracted in a higher-level
+# git repository.
+[ -e .git ] && git describe --abbrev=7 --dirty=+ 2>/dev/null | sed 's/^v//' || echo "$fallback"
diff --git a/tools/oss-fuzz.sh b/tools/oss-fuzz.sh
new file mode 100755
index 0000000..491246b
--- /dev/null
+++ b/tools/oss-fuzz.sh
@@ -0,0 +1,61 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+set -ex
+
+export LC_CTYPE=C.UTF-8
+
+export CC=${CC:-clang}
+export CXX=${CXX:-clang++}
+clang_version="$($CC --version | sed -nr 's/.*version ([^ ]+?) .*/\1/p' | sed -r 's/-$//')"
+
+SANITIZER=${SANITIZER:-address -fsanitize-address-use-after-scope}
+flags="-O1 -fno-omit-frame-pointer -gline-tables-only -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION -fsanitize=$SANITIZER"
+
+clang_lib="/usr/lib64/clang/${clang_version}/lib/linux"
+[ -d "$clang_lib" ] || clang_lib="/usr/lib/clang/${clang_version}/lib/linux"
+
+export CFLAGS=${CFLAGS:-$flags}
+export CXXFLAGS=${CXXFLAGS:-$flags}
+export LDFLAGS=${LDFLAGS:--L${clang_lib}}
+
+export WORK=${WORK:-$(pwd)}
+export OUT=${OUT:-$(pwd)/out}
+mkdir -p $OUT
+
+build=$WORK/build
+rm -rf $build
+mkdir -p $build
+
+if [ -z "$FUZZING_ENGINE" ]; then
+ fuzzflag="llvm-fuzz=true"
+else
+ fuzzflag="oss-fuzz=true"
+ if [[ "$SANITIZER" == undefined ]]; then
+ UBSAN_FLAGS="-fsanitize=pointer-overflow -fno-sanitize-recover=pointer-overflow"
+ CFLAGS="$CFLAGS $UBSAN_FLAGS"
+ CXXFLAGS="$CXXFLAGS $UBSAN_FLAGS"
+ fi
+fi
+
+meson $build -D$fuzzflag -Db_lundef=false
+ninja -v -C $build fuzzers
+
+# The seed corpus is a separate flat archive for each fuzzer,
+# with a fixed name ${fuzzer}_seed_corpus.zip.
+for d in "$(dirname "$0")/../test/fuzz/fuzz-"*; do
+ zip -jqr $OUT/$(basename "$d")_seed_corpus.zip "$d"
+done
+
+# get fuzz-dns-packet corpus
+df=$build/dns-fuzzing
+git clone --depth 1 https://github.com/CZ-NIC/dns-fuzzing $df
+zip -jqr $OUT/fuzz-dns-packet_seed_corpus.zip $df/packet
+
+install -Dt $OUT/src/shared/ $build/src/shared/libsystemd-shared-*.so
+
+wget -O $OUT/fuzz-json.dict https://raw.githubusercontent.com/rc0r/afl-fuzz/master/dictionaries/json.dict
+
+find $build -maxdepth 1 -type f -executable -name "fuzz-*" -exec mv {} $OUT \;
+find src -type f -name "fuzz-*.dict" -exec cp {} $OUT \;
+cp src/fuzz/*.options $OUT
diff --git a/tools/syscall-names-update.sh b/tools/syscall-names-update.sh
new file mode 100755
index 0000000..c884b93
--- /dev/null
+++ b/tools/syscall-names-update.sh
@@ -0,0 +1,6 @@
+#!/bin/sh
+set -eu
+
+cd "$1"
+
+curl -L -o syscall-names.text 'https://raw.githubusercontent.com/hrw/syscalls-table/master/syscall-names.text'
diff --git a/tools/update-dbus-docs.py b/tools/update-dbus-docs.py
new file mode 100755
index 0000000..ea05f5d
--- /dev/null
+++ b/tools/update-dbus-docs.py
@@ -0,0 +1,333 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+import argparse
+import collections
+import sys
+import os
+import subprocess
+import io
+
+try:
+ from lxml import etree
+except ModuleNotFoundError as e:
+ etree = e
+
+try:
+ from shlex import join as shlex_join
+except ImportError as e:
+ shlex_join = e
+
+try:
+ from shlex import quote as shlex_quote
+except ImportError as e:
+ shlex_quote = e
+
+class NoCommand(Exception):
+ pass
+
+BORING_INTERFACES = [
+ 'org.freedesktop.DBus.Peer',
+ 'org.freedesktop.DBus.Introspectable',
+ 'org.freedesktop.DBus.Properties',
+]
+
+def xml_parser():
+ return etree.XMLParser(no_network=True,
+ remove_comments=False,
+ strip_cdata=False,
+ resolve_entities=False)
+
+def print_method(declarations, elem, *, prefix, file, is_signal=False):
+ name = elem.get('name')
+ klass = 'signal' if is_signal else 'method'
+ declarations[klass].append(name)
+
+ print(f'''{prefix}{name}(''', file=file, end='')
+ lead = ',\n' + prefix + ' ' * len(name) + ' '
+
+ for num, arg in enumerate(elem.findall('./arg')):
+ argname = arg.get('name')
+
+ if argname is None:
+ if opts.print_errors:
+ print(f'method {name}: argument {num+1} has no name', file=sys.stderr)
+ argname = 'UNNAMED'
+
+ type = arg.get('type')
+ if not is_signal:
+ direction = arg.get('direction')
+ print(f'''{lead if num > 0 else ''}{direction:3} {type} {argname}''', file=file, end='')
+ else:
+ print(f'''{lead if num > 0 else ''}{type} {argname}''', file=file, end='')
+
+ print(f');', file=file)
+
+ACCESS_MAP = {
+ 'read' : 'readonly',
+ 'write' : 'readwrite',
+}
+
+def value_ellipsis(type):
+ if type == 's':
+ return "'...'";
+ if type[0] == 'a':
+ inner = value_ellipsis(type[1:])
+ return f"[{inner}{', ...' if inner != '...' else ''}]";
+ return '...'
+
+def print_property(declarations, elem, *, prefix, file):
+ name = elem.get('name')
+ type = elem.get('type')
+ access = elem.get('access')
+
+ declarations['property'].append(name)
+
+ # @org.freedesktop.DBus.Property.EmitsChangedSignal("false")
+ # @org.freedesktop.systemd1.Privileged("true")
+ # readwrite b EnableWallMessages = false;
+
+ for anno in elem.findall('./annotation'):
+ anno_name = anno.get('name')
+ anno_value = anno.get('value')
+ print(f'''{prefix}@{anno_name}("{anno_value}")''', file=file)
+
+ access = ACCESS_MAP.get(access, access)
+ print(f'''{prefix}{access} {type} {name} = {value_ellipsis(type)};''', file=file)
+
+def print_interface(iface, *, prefix, file, print_boring, only_interface, declarations):
+ name = iface.get('name')
+
+ is_boring = (name in BORING_INTERFACES or
+ only_interface is not None and name != only_interface)
+
+ if is_boring and print_boring:
+ print(f'''{prefix}interface {name} {{ ... }};''', file=file)
+
+ elif not is_boring and not print_boring:
+ print(f'''{prefix}interface {name} {{''', file=file)
+ prefix2 = prefix + ' '
+
+ for num, elem in enumerate(iface.findall('./method')):
+ if num == 0:
+ print(f'''{prefix2}methods:''', file=file)
+ print_method(declarations, elem, prefix=prefix2 + ' ', file=file)
+
+ for num, elem in enumerate(iface.findall('./signal')):
+ if num == 0:
+ print(f'''{prefix2}signals:''', file=file)
+ print_method(declarations, elem, prefix=prefix2 + ' ', file=file, is_signal=True)
+
+ for num, elem in enumerate(iface.findall('./property')):
+ if num == 0:
+ print(f'''{prefix2}properties:''', file=file)
+ print_property(declarations, elem, prefix=prefix2 + ' ', file=file)
+
+ print(f'''{prefix}}};''', file=file)
+
+def document_has_elem_with_text(document, elem, item_repr):
+ predicate = f".//{elem}" # [text() = 'foo'] doesn't seem supported :(
+ for loc in document.findall(predicate):
+ if loc.text == item_repr:
+ return True
+ return False
+
+def check_documented(document, declarations, stats):
+ missing = []
+ for klass, items in declarations.items():
+ stats['total'] += len(items)
+
+ for item in items:
+ if klass == 'method':
+ elem = 'function'
+ item_repr = f'{item}()'
+ elif klass == 'signal':
+ elem = 'function'
+ item_repr = item
+ elif klass == 'property':
+ elem = 'varname'
+ item_repr = item
+ else:
+ assert False, (klass, item)
+
+ if not document_has_elem_with_text(document, elem, item_repr):
+ if opts.print_errors:
+ print(f'{klass} {item} is not documented :(')
+ missing.append((klass, item))
+
+ stats['missing'] += len(missing)
+
+ return missing
+
+def xml_to_text(destination, xml, *, only_interface=None):
+ file = io.StringIO()
+
+ declarations = collections.defaultdict(list)
+ interfaces = []
+
+ print(f'''node {destination} {{''', file=file)
+
+ for print_boring in [False, True]:
+ for iface in xml.findall('./interface'):
+ print_interface(iface, prefix=' ', file=file,
+ print_boring=print_boring,
+ only_interface=only_interface,
+ declarations=declarations)
+ name = iface.get('name')
+ if not name in BORING_INTERFACES:
+ interfaces.append(name)
+
+ print(f'''}};''', file=file)
+
+ return file.getvalue(), declarations, interfaces
+
+def subst_output(document, programlisting, stats):
+ executable = programlisting.get('executable', None)
+ if executable is None:
+ # Not our thing
+ return
+ executable = programlisting.get('executable')
+ node = programlisting.get('node')
+ interface = programlisting.get('interface')
+
+ argv = [f'{opts.build_dir}/{executable}', f'--bus-introspect={interface}']
+ if isinstance(shlex_join, Exception):
+ print(f'COMMAND: {" ".join(shlex_quote(arg) for arg in argv)}')
+ else:
+ print(f'COMMAND: {shlex_join(argv)}')
+
+ try:
+ out = subprocess.check_output(argv, universal_newlines=True)
+ except FileNotFoundError:
+ print(f'{executable} not found, ignoring', file=sys.stderr)
+ return
+
+ xml = etree.fromstring(out, parser=xml_parser())
+
+ new_text, declarations, interfaces = xml_to_text(node, xml, only_interface=interface)
+ programlisting.text = '\n' + new_text + ' '
+
+ if declarations:
+ missing = check_documented(document, declarations, stats)
+ parent = programlisting.getparent()
+
+ # delete old comments
+ for child in parent:
+ if (child.tag == etree.Comment
+ and 'Autogenerated' in child.text):
+ parent.remove(child)
+ if (child.tag == etree.Comment
+ and 'not documented' in child.text):
+ parent.remove(child)
+ if (child.tag == "variablelist"
+ and child.attrib.get("generated",False) == "True"):
+ parent.remove(child)
+
+ # insert pointer for systemd-directives generation
+ the_tail = programlisting.tail #tail is erased by addnext, so save it here.
+ prev_element = etree.Comment("Autogenerated cross-references for systemd.directives, do not edit")
+ programlisting.addnext(prev_element)
+ programlisting.tail = the_tail
+
+ for interface in interfaces:
+ variablelist = etree.Element("variablelist")
+ variablelist.attrib['class'] = 'dbus-interface'
+ variablelist.attrib['generated'] = 'True'
+ variablelist.attrib['extra-ref'] = interface
+
+ prev_element.addnext(variablelist)
+ prev_element.tail = the_tail
+ prev_element = variablelist
+
+ for decl_type,decl_list in declarations.items():
+ for declaration in decl_list:
+ variablelist = etree.Element("variablelist")
+ variablelist.attrib['class'] = 'dbus-'+decl_type
+ variablelist.attrib['generated'] = 'True'
+ if decl_type == 'method' :
+ variablelist.attrib['extra-ref'] = declaration + '()'
+ else:
+ variablelist.attrib['extra-ref'] = declaration
+
+ prev_element.addnext(variablelist)
+ prev_element.tail = the_tail
+ prev_element = variablelist
+
+ last_element = etree.Comment("End of Autogenerated section")
+ prev_element.addnext(last_element)
+ prev_element.tail = the_tail
+ last_element.tail = the_tail
+
+ # insert comments for undocumented items
+ for item in reversed(missing):
+ comment = etree.Comment(f'{item[0]} {item[1]} is not documented!')
+ comment.tail = programlisting.tail
+ parent.insert(parent.index(programlisting) + 1, comment)
+
+def process(page):
+ src = open(page).read()
+ xml = etree.fromstring(src, parser=xml_parser())
+
+ # print('parsing {}'.format(name), file=sys.stderr)
+ if xml.tag != 'refentry':
+ return
+
+ stats = collections.Counter()
+
+ pls = xml.findall('.//programlisting')
+ for pl in pls:
+ subst_output(xml, pl, stats)
+
+ out_text = etree.tostring(xml, encoding='unicode')
+ # massage format to avoid some lxml whitespace handling idiosyncrasies
+ # https://bugs.launchpad.net/lxml/+bug/526799
+ out_text = (src[:src.find('<refentryinfo')] +
+ out_text[out_text.find('<refentryinfo'):] +
+ '\n')
+
+ if not opts.test:
+ with open(page, 'w') as out:
+ out.write(out_text)
+
+ return dict(stats=stats, outdated=(out_text != src))
+
+def parse_args():
+ p = argparse.ArgumentParser()
+ p.add_argument('--test', action='store_true',
+ help='only verify that everything is up2date')
+ p.add_argument('--build-dir', default='build')
+ p.add_argument('pages', nargs='+')
+ opts = p.parse_args()
+ opts.print_errors = not opts.test
+ return opts
+
+if __name__ == '__main__':
+ opts = parse_args()
+
+ for item in (etree, shlex_quote):
+ if isinstance(item, Exception):
+ print(item, file=sys.stderr)
+ exit(77 if opts.test else 1)
+
+ if not os.path.exists(f'{opts.build_dir}/systemd'):
+ exit(f"{opts.build_dir}/systemd doesn't exist. Use --build-dir=.")
+
+ stats = {page.split('/')[-1] : process(page) for page in opts.pages}
+
+ # Let's print all statistics at the end
+ mlen = max(len(page) for page in stats)
+ total = sum((item['stats'] for item in stats.values()), collections.Counter())
+ total = 'total', dict(stats=total, outdated=False)
+ outdated = []
+ for page, info in sorted(stats.items()) + [total]:
+ m = info['stats']['missing']
+ t = info['stats']['total']
+ p = page + ':'
+ c = 'OUTDATED' if info['outdated'] else ''
+ if c:
+ outdated.append(page)
+ print(f'{p:{mlen + 1}} {t - m}/{t} {c}')
+
+ if opts.test and outdated:
+ exit(f'Outdated pages: {", ".join(outdated)}\n'
+ f'Hint: ninja -C {opts.build_dir} man/update-dbus-docs')
diff --git a/tools/update-man-rules.py b/tools/update-man-rules.py
new file mode 100755
index 0000000..9e1660c
--- /dev/null
+++ b/tools/update-man-rules.py
@@ -0,0 +1,86 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+from __future__ import print_function
+import collections
+import sys
+import pprint
+from os.path import basename
+from xml_helper import xml_parse
+
+def man(page, number):
+ return '{}.{}'.format(page, number)
+
+def add_rules(rules, name):
+ xml = xml_parse(name)
+ # print('parsing {}'.format(name), file=sys.stderr)
+ if xml.getroot().tag != 'refentry':
+ return
+ conditional = xml.getroot().get('conditional') or ''
+ rulegroup = rules[conditional]
+ refmeta = xml.find('./refmeta')
+ title = refmeta.find('./refentrytitle').text
+ number = refmeta.find('./manvolnum').text
+ refnames = xml.findall('./refnamediv/refname')
+ target = man(refnames[0].text, number)
+ if title != refnames[0].text:
+ raise ValueError('refmeta and refnamediv disagree: ' + name)
+ for refname in refnames:
+ assert all(refname not in group
+ for group in rules.values()), "duplicate page name"
+ alias = man(refname.text, number)
+ rulegroup[alias] = target
+ # print('{} => {} [{}]'.format(alias, target, conditional), file=sys.stderr)
+
+def create_rules(xml_files):
+ " {conditional => {alias-name => source-name}} "
+ rules = collections.defaultdict(dict)
+ for name in xml_files:
+ try:
+ add_rules(rules, name)
+ except Exception:
+ print("Failed to process", name, file=sys.stderr)
+ raise
+ return rules
+
+def mjoin(files):
+ return ' \\\n\t'.join(sorted(files) or '#')
+
+MESON_HEADER = '''\
+# Do not edit. Generated by update-man-rules.py.
+# Update with:
+# ninja -C build man/update-man-rules
+manpages = ['''
+
+MESON_FOOTER = '''\
+]
+# Really, do not edit.'''
+
+def make_mesonfile(rules, dist_files):
+ # reformat rules as
+ # grouped = [ [name, section, [alias...], condition], ...]
+ #
+ # but first create a dictionary like
+ # lists = { (name, condition) => [alias...]
+ grouped = collections.defaultdict(list)
+ for condition, items in rules.items():
+ for alias, name in items.items():
+ group = grouped[(name, condition)]
+ if name != alias:
+ group.append(alias)
+
+ lines = [ [p[0][:-2], p[0][-1], sorted(a[:-2] for a in aliases), p[1]]
+ for p, aliases in sorted(grouped.items()) ]
+ return '\n'.join((MESON_HEADER, pprint.pformat(lines)[1:-1], MESON_FOOTER))
+
+if __name__ == '__main__':
+ pages = sys.argv[1:]
+ pages = (p for p in pages
+ if basename(p) not in {
+ 'systemd.directives.xml',
+ 'systemd.index.xml',
+ 'directives-template.xml'})
+
+ rules = create_rules(pages)
+ dist_files = (basename(p) for p in pages)
+ print(make_mesonfile(rules, dist_files))
diff --git a/tools/xml_helper.py b/tools/xml_helper.py
new file mode 100755
index 0000000..0361358
--- /dev/null
+++ b/tools/xml_helper.py
@@ -0,0 +1,20 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+from lxml import etree as tree
+
+class CustomResolver(tree.Resolver):
+ def resolve(self, url, id, context):
+ if 'custom-entities.ent' in url:
+ return self.resolve_filename('man/custom-entities.ent', context)
+
+_parser = tree.XMLParser()
+_parser.resolvers.add(CustomResolver())
+
+def xml_parse(page):
+ doc = tree.parse(page, _parser)
+ doc.xinclude()
+ return doc
+
+def xml_print(xml):
+ return tree.tostring(xml, pretty_print=True, encoding='utf-8')