Repository: eclipse-bluechi/bluechi (language: python)

Issue:
[RFE] should we keep a static version of inih library in tree?
## Describe the bug

In the early days of the project it was added inih library (https://github.com/benhoyt/inih) in the source code. Should we keep this approach? I noticed `inih` released a new version last month. Can we use such library in a linking approach to we can keep getting recent bits?  

```
commit caec5cedb26b7cec0850346f5bf581757ac5bdb6
Author: Veronika Fuxova <vfuxova@redhat.com>
Date:   Thu Dec 1 13:56:39 2022 +0100

    Add inih lib to src tree

    Adding the whole lib now. The plan is to identify what we don't need and
    progressively get rid of it as we continue to refine our use case.
```


Discussion / hints:
/cc @rhatdan @engelmi @mwperina 
So used the approach of copying the library because of simplicity :wink: 

But looking at the project, it's still quite active and, what's more important, it's packaged in [Fedora](https://koji.fedoraproject.org/koji/packageinfo?packageID=23025), it was even included in [EPEL 7/8](https://packages.fedoraproject.org/pkgs/inih/inih/) and older version is included in [AutoSD](https://autosd.sig.centos.org/AutoSD-9/nightly/repos/AutoSD/compose/AutoSD/x86_64/os/Packages/inih-49-6.el9.x86_64.rpm).

So if we would want to use the package instead of inlining it, we would need to:
1. Check if the compiled version aligns with our needs (many things in the project is configurable during the build only)
2. Add it to EPEL 9
3. Update the version in AutoSD
4. Remove inline code and adapt build process to use the library from a package instead
I would prefer to use the active upstream project rather then inline it, just so we can get the benefit of any security fixes found upstream. Should we get the library into RHEL though especially if we end up shipping hirte in RHEL?

Fix patch:
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 67aa96ab2f..60f7afc67c 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -44,6 +44,8 @@ jobs:
     steps:
       - name: Checkout sources
         uses: actions/checkout@v3
+        with:
+          submodules: 'true'
 
       - name: Check SPDX headers
         run: |
@@ -66,6 +68,8 @@ jobs:
     steps:
       - name: Checkout sources
         uses: actions/checkout@v3
+        with:
+          submodules: 'true'
 
       - name: Initialize meson configuration
         run: |
@@ -90,6 +94,8 @@ jobs:
     steps:
       - name: Checkout sources
         uses: actions/checkout@v3
+        with:
+          submodules: 'true'
 
       - name: Running markdown lint
         uses: DavidAnson/markdownlint-cli2-action@v9
@@ -107,6 +113,8 @@ jobs:
     steps:
       - name: Checkout sources
         uses: actions/checkout@v3
+        with:
+          submodules: 'true'
 
       - name: Run flake8
         run: |
@@ -126,6 +134,8 @@ jobs:
     steps:
       - name: Checkout sources
         uses: actions/checkout@v3
+        with:
+          submodules: 'true'
       
       - name: Install dependencies
         run: | 
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 2f0d3f995d..957a651a79 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -14,6 +14,8 @@ jobs:
     steps:
       - name: Checkout sources
         uses: actions/checkout@v3
+        with:
+          submodules: 'true'
       
       - name: Install dependencies
         run: | 
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000000..b6f58c7a79
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,6 @@
+[submodule "subprojects/hashmap.c"]
+	path = subprojects/hashmap.c
+	url = https://github.com/engelmi/hashmap.c.git
+[submodule "subprojects/inih"]
+	path = subprojects/inih
+	url = https://github.com/benhoyt/inih.git
diff --git a/.markdownlint-cli2.yaml b/.markdownlint-cli2.yaml
index 678adda454..5d97ac60b3 100644
--- a/.markdownlint-cli2.yaml
+++ b/.markdownlint-cli2.yaml
@@ -8,3 +8,4 @@ globs:
 
 ignores:
   - "doc/man/*.md"
+  - "subprojects/*/*.md"
diff --git a/LICENSES/README.md b/LICENSES/README.md
index c4b222eb24..d1ef80d6ee 100644
--- a/LICENSES/README.md
+++ b/LICENSES/README.md
@@ -27,7 +27,8 @@ The following exceptions apply:
 * Files `selinux/meson.build` and `selinux/build-selinux.sh` are copied from
   [flatpak](https://github.com/flatpak/flatpak) project and they are licensed under the original license,
   **LGPL-2.1-or-later**.
-* Files under `src/libhirte/ini/` are copied from [inih](https://github.com/benhoyt/inih) project and they're
-  licensed under the original license, **BSD-3-Clause**.
-* Files under `src/libhirte/hashmap/` are copied from [hashmap.c](https://github.com/tidwall/hashmap.c) project
-  and they are licensed under the original license, **MIT**.
+* Files under [`subprojects/`](../subprojects/) are integrated as git submodules and they are licensed under
+their respective original license as follows:
+
+  * [inih](https://github.com/benhoyt/inih/) located in [subprojects/inih](../subprojects/inih): **BSD-3-Clause**
+  * [hashmap.c](https://github.com/tidwall/hashmap.c) located in [subprojects/hashmap.c](../subprojects/hashmap.c): **MIT**
diff --git a/Makefile b/Makefile
index 2bc8c19c28..38d213047f 100644
--- a/Makefile
+++ b/Makefile
@@ -4,7 +4,7 @@ DESTDIR ?=
 BUILDDIR=builddir
 
 CODESPELL_PARAMS=\
-	-S Makefile,imgtype,copy,AUTHORS,bin,.git,CHANGELOG.md,changelog.txt,.cirrus.yml,"*.xz,*.gz,*.tar,*.tgz,*ico,*.png,*.1,*.5,*.orig,*.rej,*.xml,*xsl",build.ninja,intro-targets.json,./tests/tests/tier0/proxy-service-fails-on-typo-in-file/systemd/simple.service,tags,./builddir\
+	-S Makefile,imgtype,copy,AUTHORS,bin,.git,CHANGELOG.md,changelog.txt,.cirrus.yml,"*.xz,*.gz,*.tar,*.tgz,*ico,*.png,*.1,*.5,*.orig,*.rej,*.xml,*xsl",build.ninja,intro-targets.json,./tests/tests/tier0/proxy-service-fails-on-typo-in-file/systemd/simple.service,tags,./builddir,./subprojects,\
 	-L keypair,flate,uint,iff,od,ERRO
 
 build:
diff --git a/build-scripts/build-srpm.sh b/build-scripts/build-srpm.sh
index 44d9358219..0600354f36 100755
--- a/build-scripts/build-srpm.sh
+++ b/build-scripts/build-srpm.sh
@@ -25,8 +25,13 @@ sed \
 
 # Prepare source archive
 [[ -d rpmbuild/SOURCES ]] || mkdir -p rpmbuild/SOURCES
-git archive --format=tar --prefix=hirte-$VERSION/ --add-file=hirte.spec HEAD \
-    | gzip -9 > rpmbuild/SOURCES/hirte-$VERSION.tar.gz
+
+git archive --format=tar --prefix=hirte-$VERSION/ --add-file=hirte.spec -o hirte-$VERSION.tar HEAD
+git submodule foreach --recursive \
+    "git archive --prefix=hirte-$VERSION/\$path/ --output=\$sha1.tar HEAD && \
+     tar --concatenate --file=$(pwd)/hirte-$VERSION.tar \$sha1.tar && rm \$sha1.tar"
+gzip hirte-$VERSION.tar
+mv hirte-$VERSION.tar.gz rpmbuild/SOURCES/
 
 # Build source package
 rpmbuild \
diff --git a/build-scripts/clang-tidy-all.sh b/build-scripts/clang-tidy-all.sh
index 445704b8cd..108eb65fbb 100644
--- a/build-scripts/clang-tidy-all.sh
+++ b/build-scripts/clang-tidy-all.sh
@@ -6,6 +6,7 @@ additional_opts=$1
 SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
 SRC_DIR=$SCRIPT_DIR/../src
 BUILD_DIR=$SCRIPT_DIR/../builddir
+SUBPROJECTS_DIR=$SCRIPT_DIR/../subprojects
 
 ALL_SRC_FILES=$(find $SRC_DIR -name "*.[ch]" \
         ! -path "$SRC_DIR/libhirte/ini/ini.[ch]" \
@@ -18,5 +19,10 @@ do
     echo "Linting $src_file..."
 
     # Header config.h is autogenerated during the build, so it needs to be added manually here as a parameter
-    clang-tidy $additional_opts --quiet $src_file -- -I $SRC_DIR -I $BUILD_DIR -include config.h
+    clang-tidy $additional_opts --quiet $src_file -- \
+        -I $SRC_DIR \
+        -I $BUILD_DIR \
+        -I $SUBPROJECTS_DIR/hashmap.c \
+        -I $SUBPROJECTS_DIR/inih \
+        -include config.h
 done
diff --git a/build-scripts/spdx-header-chech.sh b/build-scripts/spdx-header-chech.sh
index 745dc0ad5b..eda45d0a81 100755
--- a/build-scripts/spdx-header-chech.sh
+++ b/build-scripts/spdx-header-chech.sh
@@ -22,6 +22,7 @@ result=0
 found_files="
     $(find -type f -regex ${CHECKED_FILES} \
         -not -path './builddir/*' \
+        -not -path './subprojects/*' \
         -not -path './doc/arch/*')
 "
 
diff --git a/meson.build b/meson.build
index a195aaccf4..d72dd7ab32 100644
--- a/meson.build
+++ b/meson.build
@@ -27,14 +27,6 @@ if api_bus == 'user'
    common_cflags += '-DUSE_USER_API_BUS'
 endif
 
-# inih library options
-# Set option of maximum line length
-# 500 characters for actual value
-# 20 characters as buffer (key, library internal usage, etc.)
-common_cflags += '-DINI_MAX_LINE=520'
-# Set option to abort parsing on first error
-common_cflags += '-DINI_STOP_ON_FIRST_ERROR=1'
-
 # Build time configuration header file
 conf = configuration_data()
 
@@ -55,6 +47,22 @@ config_h = configure_file(
 add_project_arguments('-include', 'config.h', language : 'c')
 
 
+# (External) subprojects used by hirte
+inih = subproject('inih', default_options: [
+  'default_library=static',           # include it as static library
+  '-Dinih:distro_install=false',      # disable distro install so other options are used
+  '-Dinih:max_line_length=520',       # set maximum line length so that ~500 characters can be used as value
+  '-Dinih:stop_on_first_error=true',  # abort after first error when parsing file
+])
+inih_dep = inih.get_variable('inih_dep')
+
+hashmapc = subproject('hashmap.c', default_options: [
+  'default_library=static', # include it as static library
+  'werror=false'            # don't treat warnings as errors since it
+])
+hashmapc_dep = hashmapc.get_variable('hashmapc_dep')
+
+
 # Subdirectory for the shared library.
 subdir('src/libhirte')
 
diff --git a/src/agent/agent.h b/src/agent/agent.h
index ddd8f275f5..fee6c88248 100644
--- a/src/agent/agent.h
+++ b/src/agent/agent.h
@@ -5,9 +5,10 @@
 #include <stdbool.h>
 #include <sys/socket.h>
 
+#include <hashmap.h>
+
 #include "libhirte/common/cfg.h"
 #include "libhirte/common/common.h"
-#include "libhirte/hashmap/hashmap.h"
 
 #include "types.h"
 
diff --git a/src/agent/meson.build b/src/agent/meson.build
index 6c26068114..7733a65d34 100644
--- a/src/agent/meson.build
+++ b/src/agent/meson.build
@@ -13,6 +13,8 @@ executable(
   node_src,
   dependencies: [
     systemd_dep,
+    inih_dep,
+    hashmapc_dep,
   ],
   link_with: [
     hirte_lib,
diff --git a/src/libhirte/common/cfg.c b/src/libhirte/common/cfg.c
index 8235240875..98f9068084 100644
--- a/src/libhirte/common/cfg.c
+++ b/src/libhirte/common/cfg.c
@@ -8,10 +8,11 @@
 #include <string.h>
 #include <unistd.h>
 
+#include <hashmap.h>
+#include <ini.h>
+
 #include "libhirte/common/list.h"
 #include "libhirte/common/network.h"
-#include "libhirte/hashmap/hashmap.h"
-#include "libhirte/ini/ini.h"
 
 #include "cfg.h"
 #include "common.h"
diff --git a/src/libhirte/hashmap/LICENSE b/src/libhirte/hashmap/LICENSE
deleted file mode 100644
index 541ae74de2..0000000000
--- a/src/libhirte/hashmap/LICENSE
+++ /dev/null
@@ -1,20 +0,0 @@
-The MIT License (MIT)
-
-Copyright (c) 2020 Joshua J Baker
-
-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.
\ No newline at end of file
diff --git a/src/libhirte/hashmap/hashmap.c b/src/libhirte/hashmap/hashmap.c
deleted file mode 100644
index 59089457d8..0000000000
--- a/src/libhirte/hashmap/hashmap.c
+++ /dev/null
@@ -1,1097 +0,0 @@
-/* SPDX-License-Identifier: MIT */
-// Copyright 2020 Joshua J Baker. All rights reserved.
-// Use of this source code is governed by an MIT-style
-// license that can be found in the LICENSE file.
-
-#include <stddef.h>
-#include <stdint.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-
-#include "libhirte/common/common.h"
-
-#include "hashmap.h"
-
-static void *(*_malloc)(size_t) = NULL;
-static void *(*_realloc)(void *, size_t) = NULL;
-static void (*_free)(void *) = NULL;
-
-// hashmap_set_allocator allows for configuring a custom allocator for
-// all hashmap library operations. This function, if needed, should be called
-// only once at startup and a prior to calling hashmap_new().
-void hashmap_set_allocator(void *(*malloc)(size_t), void (*free)(void *)) {
-        _malloc = malloc;
-        _free = free;
-}
-
-#define panic(_msg_)                                                                 \
-        {                                                                            \
-                fprintf(stderr, "panic: %s (%s:%d)\n", (_msg_), __FILE__, __LINE__); \
-                exit(1);                                                             \
-        }
-
-struct bucket {
-        uint64_t hash : 48;
-        uint64_t dib : 16;
-};
-
-// hashmap is an open addressed hash map using robinhood hashing.
-struct hashmap {
-        void *(*malloc)(size_t);
-        void *(*realloc)(void *, size_t);
-        void (*free)(void *);
-        bool oom;
-        size_t elsize;
-        size_t cap;
-        uint64_t seed0;
-        uint64_t seed1;
-        uint64_t (*hash)(const void *item, uint64_t seed0, uint64_t seed1);
-        int (*compare)(const void *a, const void *b, void *udata);
-        void (*elfree)(void *item);
-        void *udata;
-        size_t bucketsz;
-        size_t nbuckets;
-        size_t count;
-        size_t mask;
-        size_t growat;
-        size_t shrinkat;
-        void *buckets;
-        void *spare;
-        void *edata;
-};
-
-static struct bucket *bucket_at(struct hashmap *map, size_t index) {
-        return (struct bucket *) (((char *) map->buckets) + (map->bucketsz * index));
-}
-
-static void *bucket_item(struct bucket *entry) {
-        return ((char *) entry) + sizeof(struct bucket);
-}
-
-static uint64_t get_hash(struct hashmap *map, const void *key) {
-        return map->hash(key, map->seed0, map->seed1) << 16 >> 16;
-}
-
-// hashmap_new_with_allocator returns a new hash map using a custom allocator.
-// See hashmap_new for more information.
-struct hashmap *hashmap_new_with_allocator(
-                void *(*_malloc)(size_t),
-                void *(*_realloc)(void *, size_t),
-                void (*_free)(void *),
-                size_t elsize,
-                size_t cap,
-                uint64_t seed0,
-                uint64_t seed1,
-                uint64_t (*hash)(const void *item, uint64_t seed0, uint64_t seed1),
-                int (*compare)(const void *a, const void *b, void *udata),
-                void (*elfree)(void *item),
-                void *udata) {
-        _malloc = _malloc ? _malloc : malloc;
-        _realloc = _realloc ? _realloc : realloc;
-        _free = _free ? _free : free;
-        size_t ncap = 16;
-        if (cap < ncap) {
-                cap = ncap;
-        } else {
-                while (ncap < cap) {
-                        ncap *= 2;
-                }
-                cap = ncap;
-        }
-        size_t bucketsz = sizeof(struct bucket) + elsize;
-        while (bucketsz & (sizeof(uintptr_t) - 1)) {
-                bucketsz++;
-        }
-        // hashmap + spare + edata
-        size_t size = sizeof(struct hashmap) + bucketsz * 2;
-        struct hashmap *map = _malloc(size);
-        if (!map) {
-                return NULL;
-        }
-        memset(map, 0, sizeof(struct hashmap));
-        map->elsize = elsize;
-        map->bucketsz = bucketsz;
-        map->seed0 = seed0;
-        map->seed1 = seed1;
-        map->hash = hash;
-        map->compare = compare;
-        map->elfree = elfree;
-        map->udata = udata;
-        map->spare = ((char *) map) + sizeof(struct hashmap);
-        map->edata = (char *) map->spare + bucketsz;
-        map->cap = cap;
-        map->nbuckets = cap;
-        map->mask = map->nbuckets - 1;
-        map->buckets = _malloc(map->bucketsz * map->nbuckets);
-        if (!map->buckets) {
-                _free(map);
-                return NULL;
-        }
-        memset(map->buckets, 0, map->bucketsz * map->nbuckets);
-        map->growat = map->nbuckets * 0.75;
-        map->shrinkat = map->nbuckets * 0.10;
-        map->malloc = _malloc;
-        map->realloc = _realloc;
-        map->free = _free;
-        return map;
-}
-
-
-// hashmap_new returns a new hash map.
-// Param `elsize` is the size of each element in the tree. Every element that
-// is inserted, deleted, or retrieved will be this size.
-// Param `cap` is the default lower capacity of the hashmap. Setting this to
-// zero will default to 16.
-// Params `seed0` and `seed1` are optional seed values that are passed to the
-// following `hash` function. These can be any value you wish but it's often
-// best to use randomly generated values.
-// Param `hash` is a function that generates a hash value for an item. It's
-// important that you provide a good hash function, otherwise it will perform
-// poorly or be vulnerable to Denial-of-service attacks. This implementation
-// comes with two helper functions `hashmap_sip()` and `hashmap_murmur()`.
-// Param `compare` is a function that compares items in the tree. See the
-// qsort stdlib function for an example of how this function works.
-// The hashmap must be freed with hashmap_free().
-// Param `elfree` is a function that frees a specific item. This should be NULL
-// unless you're storing some kind of reference data in the hash.
-struct hashmap *
-                hashmap_new(size_t elsize,
-                            size_t cap,
-                            uint64_t seed0,
-                            uint64_t seed1,
-                            uint64_t (*hash)(const void *item, uint64_t seed0, uint64_t seed1),
-                            int (*compare)(const void *a, const void *b, void *udata),
-                            void (*elfree)(void *item),
-                            void *udata) {
-        return hashmap_new_with_allocator(
-                        (_malloc ? _malloc : malloc),
-                        (_realloc ? _realloc : realloc),
-                        (_free ? _free : free),
-                        elsize,
-                        cap,
-                        seed0,
-                        seed1,
-                        hash,
-                        compare,
-                        elfree,
-                        udata);
-}
-
-static void free_elements(struct hashmap *map) {
-        if (map->elfree) {
-                for (size_t i = 0; i < map->nbuckets; i++) {
-                        struct bucket *bucket = bucket_at(map, i);
-                        if (bucket->dib)
-                                map->elfree(bucket_item(bucket));
-                }
-        }
-}
-
-
-// hashmap_clear quickly clears the map.
-// Every item is called with the element-freeing function given in hashmap_new,
-// if present, to free any data referenced in the elements of the hashmap.
-// When the update_cap is provided, the map's capacity will be updated to match
-// the current number of allocated buckets. This is an optimization to ensure
-// that this operation does not perform any allocations.
-void hashmap_clear(struct hashmap *map, bool update_cap) {
-        map->count = 0;
-        free_elements(map);
-        if (update_cap) {
-                map->cap = map->nbuckets;
-        } else if (map->nbuckets != map->cap) {
-                void *new_buckets = map->malloc(map->bucketsz * map->cap);
-                if (new_buckets) {
-                        map->free(map->buckets);
-                        map->buckets = new_buckets;
-                }
-                map->nbuckets = map->cap;
-        }
-        memset(map->buckets, 0, map->bucketsz * map->nbuckets);
-        map->mask = map->nbuckets - 1;
-        map->growat = map->nbuckets * 0.75;
-        map->shrinkat = map->nbuckets * 0.10;
-}
-
-
-static bool resize(struct hashmap *map, size_t new_cap) {
-        struct hashmap *map2 = hashmap_new_with_allocator(
-                        map->malloc,
-                        map->realloc,
-                        map->free,
-                        map->elsize,
-                        new_cap,
-                        map->seed0,
-                        map->seed1,
-                        map->hash,
-                        map->compare,
-                        map->elfree,
-                        map->udata);
-        if (!map2) {
-                return false;
-        }
-        for (size_t i = 0; i < map->nbuckets; i++) {
-                struct bucket *entry = bucket_at(map, i);
-                if (!entry->dib) {
-                        continue;
-                }
-                entry->dib = 1;
-                size_t j = entry->hash & map2->mask;
-                for (;;) {
-                        struct bucket *bucket = bucket_at(map2, j);
-                        if (bucket->dib == 0) {
-                                memcpy(bucket, entry, map->bucketsz);
-                                break;
-                        }
-                        if (bucket->dib < entry->dib) {
-                                memcpy(map2->spare, bucket, map->bucketsz);
-                                memcpy(bucket, entry, map->bucketsz);
-                                memcpy(entry, map2->spare, map->bucketsz);
-                        }
-                        j = (j + 1) & map2->mask;
-                        entry->dib += 1;
-                }
-        }
-        map->free(map->buckets);
-        map->buckets = map2->buckets;
-        map->nbuckets = map2->nbuckets;
-        map->mask = map2->mask;
-        map->growat = map2->growat;
-        map->shrinkat = map2->shrinkat;
-        map->free(map2);
-        return true;
-}
-
-// hashmap_set inserts or replaces an item in the hash map. If an item is
-// replaced then it is returned otherwise NULL is returned. This operation
-// may allocate memory. If the system is unable to allocate additional
-// memory then NULL is returned and hashmap_oom() returns true.
-void *hashmap_set(struct hashmap *map, const void *item) {
-        if (!item) {
-                panic("item is null");
-        }
-        map->oom = false;
-        if (map->count == map->growat) {
-                if (!resize(map, map->nbuckets * 2)) {
-                        map->oom = true;
-                        return NULL;
-                }
-        }
-
-
-        struct bucket *entry = map->edata;
-        entry->hash = get_hash(map, item);
-        entry->dib = 1;
-        memcpy(bucket_item(entry), item, map->elsize);
-
-        size_t i = entry->hash & map->mask;
-        for (;;) {
-                struct bucket *bucket = bucket_at(map, i);
-                if (bucket->dib == 0) {
-                        memcpy(bucket, entry, map->bucketsz);
-                        map->count++;
-                        return NULL;
-                }
-                if (entry->hash == bucket->hash &&
-                    map->compare(bucket_item(entry), bucket_item(bucket), map->udata) == 0) {
-                        memcpy(map->spare, bucket_item(bucket), map->elsize);
-                        memcpy(bucket_item(bucket), bucket_item(entry), map->elsize);
-                        return map->spare;
-                }
-                if (bucket->dib < entry->dib) {
-                        memcpy(map->spare, bucket, map->bucketsz);
-                        memcpy(bucket, entry, map->bucketsz);
-                        memcpy(entry, map->spare, map->bucketsz);
-                }
-                i = (i + 1) & map->mask;
-                entry->dib += 1;
-        }
-}
-
-// hashmap_get returns the item based on the provided key. If the item is not
-// found then NULL is returned.
-void *hashmap_get(struct hashmap *map, const void *key) {
-        if (!key) {
-                panic("key is null");
-        }
-        uint64_t hash = get_hash(map, key);
-        size_t i = hash & map->mask;
-        for (;;) {
-                struct bucket *bucket = bucket_at(map, i);
-                if (!bucket->dib) {
-                        return NULL;
-                }
-                if (bucket->hash == hash && map->compare(key, bucket_item(bucket), map->udata) == 0) {
-                        return bucket_item(bucket);
-                }
-                i = (i + 1) & map->mask;
-        }
-}
-
-// hashmap_probe returns the item in the bucket at position or NULL if an item
-// is not set for that bucket. The position is 'moduloed' by the number of
-// buckets in the hashmap.
-void *hashmap_probe(struct hashmap *map, uint64_t position) {
-        size_t i = position & map->mask;
-        struct bucket *bucket = bucket_at(map, i);
-        if (!bucket->dib) {
-                return NULL;
-        }
-        return bucket_item(bucket);
-}
-
-
-// hashmap_delete removes an item from the hash map and returns it. If the
-// item is not found then NULL is returned.
-void *hashmap_delete(struct hashmap *map, void *key) {
-        if (!key) {
-                panic("key is null");
-        }
-        map->oom = false;
-        uint64_t hash = get_hash(map, key);
-        size_t i = hash & map->mask;
-        for (;;) {
-                struct bucket *bucket = bucket_at(map, i);
-                if (!bucket->dib) {
-                        return NULL;
-                }
-                if (bucket->hash == hash && map->compare(key, bucket_item(bucket), map->udata) == 0) {
-                        memcpy(map->spare, bucket_item(bucket), map->elsize);
-                        bucket->dib = 0;
-                        for (;;) {
-                                struct bucket *prev = bucket;
-                                i = (i + 1) & map->mask;
-                                bucket = bucket_at(map, i);
-                                if (bucket->dib <= 1) {
-                                        prev->dib = 0;
-                                        break;
-                                }
-                                memcpy(prev, bucket, map->bucketsz);
-                                prev->dib--;
-                        }
-                        map->count--;
-                        if (map->nbuckets > map->cap && map->count <= map->shrinkat) {
-                                // Ignore the return value. It's ok for the resize operation to
-                                // fail to allocate enough memory because a shrink operation
-                                // does not change the integrity of the data.
-                                resize(map, map->nbuckets / 2);
-                        }
-                        return map->spare;
-                }
-                i = (i + 1) & map->mask;
-        }
-}
-
-// hashmap_count returns the number of items in the hash map.
-size_t hashmap_count(struct hashmap *map) {
-        return map->count;
-}
-
-// hashmap_free frees the hash map
-// Every item is called with the element-freeing function given in hashmap_new,
-// if present, to free any data referenced in the elements of the hashmap.
-void hashmap_free(struct hashmap *map) {
-        if (!map)
-                return;
-        free_elements(map);
-        map->free(map->buckets);
-        map->free(map);
-}
-
-// hashmap_oom returns true if the last hashmap_set() call failed due to the
-// system being out of memory.
-bool hashmap_oom(struct hashmap *map) {
-        return map->oom;
-}
-
-// hashmap_scan iterates over all items in the hash map
-// Param `iter` can return false to stop iteration early.
-// Returns false if the iteration has been stopped early.
-bool hashmap_scan(struct hashmap *map, bool (*iter)(const void *item, void *udata), void *udata) {
-        for (size_t i = 0; i < map->nbuckets; i++) {
-                struct bucket *bucket = bucket_at(map, i);
-                if (bucket->dib) {
-                        if (!iter(bucket_item(bucket), udata)) {
-                                return false;
-                        }
-                }
-        }
-        return true;
-}
-
-
-// hashmap_iter iterates one key at a time yielding a reference to an
-// entry at each iteration. Useful to write simple loops and avoid writing
-// dedicated callbacks and udata structures, as in hashmap_scan.
-//
-// map is a hash map handle. i is a pointer to a size_t cursor that
-// should be initialized to 0 at the beginning of the loop. item is a void
-// pointer pointer that is populated with the retrieved item. Note that this
-// is NOT a copy of the item stored in the hash map and can be directly
-// modified.
-//
-// Note that if hashmap_delete() is called on the hashmap being iterated,
-// the buckets are rearranged and the iterator must be reset to 0, otherwise
-// unexpected results may be returned after deletion.
-//
-// This function has not been tested for thread safety.
-//
-// The function returns true if an item was retrieved; false if the end of the
-// iteration has been reached.
-bool hashmap_iter(struct hashmap *map, size_t *i, void **item) {
-        struct bucket *bucket;
-
-        do {
-                if (*i >= map->nbuckets)
-                        return false;
-
-                bucket = bucket_at(map, *i);
-                (*i)++;
-        } while (!bucket->dib);
-
-        *item = bucket_item(bucket);
-
-        return true;
-}
-
-
-//-----------------------------------------------------------------------------
-// SipHash reference C implementation
-//
-// Copyright (c) 2012-2016 Jean-Philippe Aumasson
-// <jeanphilippe.aumasson@gmail.com>
-// Copyright (c) 2012-2014 Daniel J. Bernstein <djb@cr.yp.to>
-//
-// To the extent possible under law, the author(s) have dedicated all copyright
-// and related and neighboring rights to this software to the public domain
-// worldwide. This software is distributed without any warranty.
-//
-// You should have received a copy of the CC0 Public Domain Dedication along
-// with this software. If not, see
-// <http://creativecommons.org/publicdomain/zero/1.0/>.
-//
-// default: SipHash-2-4
-//-----------------------------------------------------------------------------
-static uint64_t SIP64(const uint8_t *in, const size_t inlen, uint64_t seed0, uint64_t seed1) {
-#define U8TO64_LE(p)                                                                                 \
-        { (((uint64_t) ((p)[0])) | ((uint64_t) ((p)[1]) << 8) | ((uint64_t) ((p)[2]) << 16) |        \
-           ((uint64_t) ((p)[3]) << 24) | ((uint64_t) ((p)[4]) << 32) | ((uint64_t) ((p)[5]) << 40) | \
-           ((uint64_t) ((p)[6]) << 48) | ((uint64_t) ((p)[7]) << 56)) }
-#define U64TO8_LE(p, v)                                     \
-        {                                                   \
-                U32TO8_LE((p), (uint32_t) ((v)));           \
-                U32TO8_LE((p) + 4, (uint32_t) ((v) >> 32)); \
-        }
-#define U32TO8_LE(p, v)                         \
-        {                                       \
-                (p)[0] = (uint8_t) ((v));       \
-                (p)[1] = (uint8_t) ((v) >> 8);  \
-                (p)[2] = (uint8_t) ((v) >> 16); \
-                (p)[3] = (uint8_t) ((v) >> 24); \
-        }
-#define ROTL(x, b) (uint64_t)(((x) << (b)) | ((x) >> (64 - (b))))
-#define SIPROUND                   \
-        {                          \
-                v0 += v1;          \
-                v1 = ROTL(v1, 13); \
-                v1 ^= v0;          \
-                v0 = ROTL(v0, 32); \
-                v2 += v3;          \
-                v3 = ROTL(v3, 16); \
-                v3 ^= v2;          \
-                v0 += v3;          \
-                v3 = ROTL(v3, 21); \
-                v3 ^= v0;          \
-                v2 += v1;          \
-                v1 = ROTL(v1, 17); \
-                v1 ^= v2;          \
-                v2 = ROTL(v2, 32); \
-        }
-        uint64_t k0 = U8TO64_LE((uint8_t *) &seed0);
-        uint64_t k1 = U8TO64_LE((uint8_t *) &seed1);
-        uint64_t v3 = UINT64_C(0x7465646279746573) ^ k1;
-        uint64_t v2 = UINT64_C(0x6c7967656e657261) ^ k0;
-        uint64_t v1 = UINT64_C(0x646f72616e646f6d) ^ k1;
-        uint64_t v0 = UINT64_C(0x736f6d6570736575) ^ k0;
-        const uint8_t *end = in + inlen - (inlen % sizeof(uint64_t));
-        for (; in != end; in += 8) {
-                uint64_t m = U8TO64_LE(in);
-                v3 ^= m;
-                SIPROUND;
-                SIPROUND;
-                v0 ^= m;
-        }
-        const int left = inlen & 7;
-        uint64_t b = ((uint64_t) inlen) << 56;
-        switch (left) {
-        case 7:
-                b |= ((uint64_t) in[6]) << 48;
-                [[fallthrough]];
-        case 6:
-                b |= ((uint64_t) in[5]) << 40;
-                [[fallthrough]];
-        case 5:
-                b |= ((uint64_t) in[4]) << 32;
-                [[fallthrough]];
-        case 4:
-                b |= ((uint64_t) in[3]) << 24;
-                [[fallthrough]];
-        case 3:
-                b |= ((uint64_t) in[2]) << 16;
-                [[fallthrough]];
-        case 2:
-                b |= ((uint64_t) in[1]) << 8;
-                [[fallthrough]];
-        case 1:
-                b |= ((uint64_t) in[0]);
-                break;
-        case 0:
-                break;
-        }
-        v3 ^= b;
-        SIPROUND;
-        SIPROUND;
-        v0 ^= b;
-        v2 ^= 0xff;
-        SIPROUND;
-        SIPROUND;
-        SIPROUND;
-        SIPROUND;
-        b = v0 ^ v1 ^ v2 ^ v3;
-        uint64_t out = 0;
-        U64TO8_LE((uint8_t *) &out, b);
-        return out;
-}
-
-//-----------------------------------------------------------------------------
-// MurmurHash3 was written by Austin Appleby, and is placed in the public
-// domain. The author hereby disclaims copyright to this source code.
-//
-// Murmur3_86_128
-//-----------------------------------------------------------------------------
-static void MM86128(const void *key, const int len, uint32_t seed, void *out) {
-#define ROTL32(x, r) ((x << r) | (x >> (32 - r)))
-#define FMIX32(h)        \
-        h ^= h >> 16;    \
-        h *= 0x85ebca6b; \
-        h ^= h >> 13;    \
-        h *= 0xc2b2ae35; \
-        h ^= h >> 16;
-        const uint8_t *data = (const uint8_t *) key;
-        const int nblocks = len / 16;
-        uint32_t h1 = seed;
-        uint32_t h2 = seed;
-        uint32_t h3 = seed;
-        uint32_t h4 = seed;
-        uint32_t c1 = 0x239b961b;
-        uint32_t c2 = 0xab0e9789;
-        uint32_t c3 = 0x38b34ae5;
-        uint32_t c4 = 0xa1e38b93;
-        const uint32_t *blocks = (const uint32_t *) (data + nblocks * 16);
-        for (int i = -nblocks; i; i++) {
-                uint32_t k1 = blocks[i * 4 + 0];
-                uint32_t k2 = blocks[i * 4 + 1];
-                uint32_t k3 = blocks[i * 4 + 2];
-                uint32_t k4 = blocks[i * 4 + 3];
-                k1 *= c1;
-                k1 = ROTL32(k1, 15);
-                k1 *= c2;
-                h1 ^= k1;
-                h1 = ROTL32(h1, 19);
-                h1 += h2;
-                h1 = h1 * 5 + 0x561ccd1b;
-                k2 *= c2;
-                k2 = ROTL32(k2, 16);
-                k2 *= c3;
-                h2 ^= k2;
-                h2 = ROTL32(h2, 17);
-                h2 += h3;
-                h2 = h2 * 5 + 0x0bcaa747;
-                k3 *= c3;
-                k3 = ROTL32(k3, 17);
-                k3 *= c4;
-                h3 ^= k3;
-                h3 = ROTL32(h3, 15);
-                h3 += h4;
-                h3 = h3 * 5 + 0x96cd1c35;
-                k4 *= c4;
-                k4 = ROTL32(k4, 18);
-                k4 *= c1;
-                h4 ^= k4;
-                h4 = ROTL32(h4, 13);
-                h4 += h1;
-                h4 = h4 * 5 + 0x32ac3b17;
-        }
-        const uint8_t *tail = (const uint8_t *) (data + nblocks * 16);
-        uint32_t k1 = 0;
-        uint32_t k2 = 0;
-        uint32_t k3 = 0;
-        uint32_t k4 = 0;
-        switch (len & 15) {
-        case 15:
-                k4 ^= tail[14] << 16;
-                [[fallthrough]];
-        case 14:
-                k4 ^= tail[13] << 8;
-                [[fallthrough]];
-        case 13:
-                k4 ^= tail[12] << 0;
-                k4 *= c4;
-                k4 = ROTL32(k4, 18);
-                k4 *= c1;
-                h4 ^= k4;
-                [[fallthrough]];
-        case 12:
-                k3 ^= tail[11] << 24;
-                [[fallthrough]];
-        case 11:
-                k3 ^= tail[10] << 16;
-                [[fallthrough]];
-        case 10:
-                k3 ^= tail[9] << 8;
-                [[fallthrough]];
-        case 9:
-                k3 ^= tail[8] << 0;
-                k3 *= c3;
-                k3 = ROTL32(k3, 17);
-                k3 *= c4;
-                h3 ^= k3;
-                [[fallthrough]];
-        case 8:
-                k2 ^= tail[7] << 24;
-                [[fallthrough]];
-        case 7:
-                k2 ^= tail[6] << 16;
-                [[fallthrough]];
-        case 6:
-                k2 ^= tail[5] << 8;
-                [[fallthrough]];
-        case 5:
-                k2 ^= tail[4] << 0;
-                k2 *= c2;
-                k2 = ROTL32(k2, 16);
-                k2 *= c3;
-                h2 ^= k2;
-                [[fallthrough]];
-        case 4:
-                k1 ^= tail[3] << 24;
-                [[fallthrough]];
-        case 3:
-                k1 ^= tail[2] << 16;
-                [[fallthrough]];
-        case 2:
-                k1 ^= tail[1] << 8;
-                [[fallthrough]];
-        case 1:
-                k1 ^= tail[0] << 0;
-                k1 *= c1;
-                k1 = ROTL32(k1, 15);
-                k1 *= c2;
-                h1 ^= k1;
-        };
-        h1 ^= len;
-        h2 ^= len;
-        h3 ^= len;
-        h4 ^= len;
-        h1 += h2;
-        h1 += h3;
-        h1 += h4;
-        h2 += h1;
-        h3 += h1;
-        h4 += h1;
-        FMIX32(h1);
-        FMIX32(h2);
-        FMIX32(h3);
-        FMIX32(h4);
-        h1 += h2;
-        h1 += h3;
-        h1 += h4;
-        h2 += h1;
-        h3 += h1;
-        h4 += h1;
-        ((uint32_t *) out)[0] = h1;
-        ((uint32_t *) out)[1] = h2;
-        ((uint32_t *) out)[2] = h3;
-        ((uint32_t *) out)[3] = h4;
-}
-
-// hashmap_sip returns a hash value for `data` using SipHash-2-4.
-uint64_t hashmap_sip(const void *data, size_t len, uint64_t seed0, uint64_t seed1) {
-        return SIP64((uint8_t *) data, len, seed0, seed1);
-}
-
-// hashmap_murmur returns a hash value for `data` using Murmur3_86_128.
-uint64_t hashmap_murmur(const void *data, size_t len, uint64_t seed0, UNUSED uint64_t seed1) {
-        char out[16];
-        MM86128(data, len, seed0, &out);
-        return *(uint64_t *) out;
-}
-
-//==============================================================================
-// TESTS AND BENCHMARKS
-// $ cc -DHASHMAP_TEST hashmap.c && ./a.out              # run tests
-// $ cc -DHASHMAP_TEST -O3 hashmap.c && BENCH=1 ./a.out  # run benchmarks
-//==============================================================================
-#ifdef HASHMAP_TEST
-
-static size_t deepcount(struct hashmap *map) {
-        size_t count = 0;
-        for (size_t i = 0; i < map->nbuckets; i++) {
-                if (bucket_at(map, i)->dib) {
-                        count++;
-                }
-        }
-        return count;
-}
-
-
-#        pragma GCC diagnostic ignored "-Wextra"
-
-
-#        include "hashmap.h"
-#        include <assert.h>
-#        include <stdio.h>
-#        include <stdlib.h>
-#        include <string.h>
-#        include <time.h>
-
-static bool rand_alloc_fail = false;
-static int rand_alloc_fail_odds = 3; // 1 in 3 chance malloc will fail.
-static uintptr_t total_allocs = 0;
-static uintptr_t total_mem = 0;
-
-static void *xmalloc(size_t size) {
-        if (rand_alloc_fail && rand() % rand_alloc_fail_odds == 0) {
-                return NULL;
-        }
-        void *mem = malloc(sizeof(uintptr_t) + size);
-        assert(mem);
-        *(uintptr_t *) mem = size;
-        total_allocs++;
-        total_mem += size;
-        return (char *) mem + sizeof(uintptr_t);
-}
-
-static void xfree(void *ptr) {
-        if (ptr) {
-                total_mem -= *(uintptr_t *) ((char *) ptr - sizeof(uintptr_t));
-                free((char *) ptr - sizeof(uintptr_t));
-                total_allocs--;
-        }
-}
-
-static void shuffle(void *array, size_t numels, size_t elsize) {
-        char tmp[elsize];
-        char *arr = array;
-        for (size_t i = 0; i < numels - 1; i++) {
-                int j = i + rand() / (RAND_MAX / (numels - i) + 1);
-                memcpy(tmp, arr + j * elsize, elsize);
-                memcpy(arr + j * elsize, arr + i * elsize, elsize);
-                memcpy(arr + i * elsize, tmp, elsize);
-        }
-}
-
-static bool iter_ints(const void *item, void *udata) {
-        int *vals = *(int **) udata;
-        vals[*(int *) item] = 1;
-        return true;
-}
-
-static int compare_ints(const void *a, const void *b) {
-        return *(int *) a - *(int *) b;
-}
-
-static int compare_ints_udata(const void *a, const void *b, void *udata) {
-        return *(int *) a - *(int *) b;
-}
-
-static int compare_strs(const void *a, const void *b, void *udata) {
-        return strcmp(*(char **) a, *(char **) b);
-}
-
-static uint64_t hash_int(const void *item, uint64_t seed0, uint64_t seed1) {
-        return hashmap_murmur(item, sizeof(int), seed0, seed1);
-}
-
-static uint64_t hash_str(const void *item, uint64_t seed0, uint64_t seed1) {
-        return hashmap_murmur(*(char **) item, strlen(*(char **) item), seed0, seed1);
-}
-
-static void free_str(void *item) {
-        xfree(*(char **) item);
-}
-
-static void all() {
-        int seed = getenv("SEED") ? atoi(getenv("SEED")) : time(NULL);
-        int N = getenv("N") ? atoi(getenv("N")) : 2000;
-        printf("seed=%d, count=%d, item_size=%zu\n", seed, N, sizeof(int));
-        srand(seed);
-
-        rand_alloc_fail = true;
-
-        // test sip and murmur hashes
-        assert(hashmap_sip("hello", 5, 1, 2) == 2957200328589801622);
-        assert(hashmap_murmur("hello", 5, 1, 2) == 1682575153221130884);
-
-        int *vals;
-        while (!(vals = xmalloc(N * sizeof(int)))) {
-        }
-        for (int i = 0; i < N; i++) {
-                vals[i] = i;
-        }
-
-        struct hashmap *map;
-
-        while (!(map = hashmap_new(sizeof(int), 0, seed, seed, hash_int, compare_ints_udata, NULL, NULL))) {
-        }
-        shuffle(vals, N, sizeof(int));
-        for (int i = 0; i < N; i++) {
-                // // printf("== %d ==\n", vals[i]);
-                assert(map->count == i);
-                assert(map->count == hashmap_count(map));
-                assert(map->count == deepcount(map));
-                int *v;
-                assert(!hashmap_get(map, &vals[i]));
-                assert(!hashmap_delete(map, &vals[i]));
-                while (true) {
-                        assert(!hashmap_set(map, &vals[i]));
-                        if (!hashmap_oom(map)) {
-                                break;
-                        }
-                }
-
-                for (int j = 0; j < i; j++) {
-                        v = hashmap_get(map, &vals[j]);
-                        assert(v && *v == vals[j]);
-                }
-                while (true) {
-                        v = hashmap_set(map, &vals[i]);
-                        if (!v) {
-                                assert(hashmap_oom(map));
-                                continue;
-                        } else {
-                                assert(!hashmap_oom(map));
-                                assert(v && *v == vals[i]);
-                                break;
-                        }
-                }
-                v = hashmap_get(map, &vals[i]);
-                assert(v && *v == vals[i]);
-                v = hashmap_delete(map, &vals[i]);
-                assert(v && *v == vals[i]);
-                assert(!hashmap_get(map, &vals[i]));
-                assert(!hashmap_delete(map, &vals[i]));
-                assert(!hashmap_set(map, &vals[i]));
-                assert(map->count == i + 1);
-                assert(map->count == hashmap_count(map));
-                assert(map->count == deepcount(map));
-        }
-
-        int *vals2;
-        while (!(vals2 = xmalloc(N * sizeof(int)))) {
-        }
-        memset(vals2, 0, N * sizeof(int));
-        assert(hashmap_scan(map, iter_ints, &vals2));
-
-        // Test hashmap_iter. This does the same as hashmap_scan above.
-        size_t iter = 0;
-        void *iter_val;
-        while (hashmap_iter(map, &iter, &iter_val)) {
-                assert(iter_ints(iter_val, &vals2));
-        }
-        for (int i = 0; i < N; i++) {
-                assert(vals2[i] == 1);
-        }
-        xfree(vals2);
-
-        shuffle(vals, N, sizeof(int));
-        for (int i = 0; i < N; i++) {
-                int *v;
-                v = hashmap_delete(map, &vals[i]);
-                assert(v && *v == vals[i]);
-                assert(!hashmap_get(map, &vals[i]));
-                assert(map->count == N - i - 1);
-                assert(map->count == hashmap_count(map));
-                assert(map->count == deepcount(map));
-                for (int j = N - 1; j > i; j--) {
-                        v = hashmap_get(map, &vals[j]);
-                        assert(v && *v == vals[j]);
-                }
-        }
-
-        for (int i = 0; i < N; i++) {
-                while (true) {
-                        assert(!hashmap_set(map, &vals[i]));
-                        if (!hashmap_oom(map)) {
-                                break;
-                        }
-                }
-        }
-
-        assert(map->count != 0);
-        size_t prev_cap = map->cap;
-        hashmap_clear(map, true);
-        assert(prev_cap < map->cap);
-        assert(map->count == 0);
-
-
-        for (int i = 0; i < N; i++) {
-                while (true) {
-                        assert(!hashmap_set(map, &vals[i]));
-                        if (!hashmap_oom(map)) {
-                                break;
-                        }
-                }
-        }
-
-        prev_cap = map->cap;
-        hashmap_clear(map, false);
-        assert(prev_cap == map->cap);
-
-        hashmap_free(map);
-
-        xfree(vals);
-
-
-        while (!(map = hashmap_new(sizeof(char *), 0, seed, seed, hash_str, compare_strs, free_str, NULL)))
-                ;
-
-        for (int i = 0; i < N; i++) {
-                char *str;
-                while (!(str = xmalloc(16)))
-                        ;
-                sprintf(str, "s%i", i);
-                while (!hashmap_set(map, &str))
-                        ;
-        }
-
-        hashmap_clear(map, false);
-        assert(hashmap_count(map) == 0);
-
-        for (int i = 0; i < N; i++) {
-                char *str;
-                while (!(str = xmalloc(16)))
-                        ;
-                sprintf(str, "s%i", i);
-                while (!hashmap_set(map, &str))
-                        ;
-        }
-
-        hashmap_free(map);
-
-        if (total_allocs != 0) {
-                fprintf(stderr, "total_allocs: expected 0, got %lu\n", total_allocs);
-                exit(1);
-        }
-}
-
-#        define bench(name, N, code)                                                             \
-                {                                                                                \
-                        {                                                                        \
-                                if (strlen(name) > 0) {                                          \
-                                        printf("%-14s ", name);                                  \
-                                }                                                                \
-                                size_t tmem = total_mem;                                         \
-                                size_t tallocs = total_allocs;                                   \
-                                uint64_t bytes = 0;                                              \
-                                clock_t begin = clock();                                         \
-                                for (int i = 0; i < N; i++) {                                    \
-                                        (code);                                                  \
-                                }                                                                \
-                                clock_t end = clock();                                           \
-                                double elapsed_secs = (double) (end - begin) / CLOCKS_PER_SEC;   \
-                                double bytes_sec = (double) bytes / elapsed_secs;                \
-                                printf("%d ops in %.3f secs, %.0f ns/op, %.0f op/sec",           \
-                                       N,                                                        \
-                                       elapsed_secs,                                             \
-                                       elapsed_secs / (double) N * 1e9,                          \
-                                       (double) N / elapsed_secs);                               \
-                                if (bytes > 0) {                                                 \
-                                        printf(", %.1f GB/sec", bytes_sec / 1024 / 1024 / 1024); \
-                                }                                                                \
-                                if (total_mem > tmem) {                                          \
-                                        size_t used_mem = total_mem - tmem;                      \
-                                        printf(", %.2f bytes/op", (double) used_mem / N);        \
-                                }                                                                \
-                                if (total_allocs > tallocs) {                                    \
-                                        size_t used_allocs = total_allocs - tallocs;             \
-                                        printf(", %.2f allocs/op", (double) used_allocs / N);    \
-                                }                                                                \
-                                printf("\n");                                                    \
-                        }                                                                        \
-                }
-
-static void benchmarks() {
-        int seed = getenv("SEED") ? atoi(getenv("SEED")) : time(NULL);
-        int N = getenv("N") ? atoi(getenv("N")) : 5000000;
-        printf("seed=%d, count=%d, item_size=%zu\n", seed, N, sizeof(int));
-        srand(seed);
-
-
-        int *vals = xmalloc(N * sizeof(int));
-        for (int i = 0; i < N; i++) {
-                vals[i] = i;
-        }
-
-        shuffle(vals, N, sizeof(int));
-
-        struct hashmap *map;
-        shuffle(vals, N, sizeof(int));
-
-        map = hashmap_new(sizeof(int), 0, seed, seed, hash_int, compare_ints_udata, NULL, NULL);
-        bench("set", N, {
-                int *v = hashmap_set(map, &vals[i]);
-                assert(!v);
-        }) shuffle(vals, N, sizeof(int));
-        bench("get", N, {
-                int *v = hashmap_get(map, &vals[i]);
-                assert(v && *v == vals[i]);
-        }) shuffle(vals, N, sizeof(int));
-        bench("delete", N, {
-                int *v = hashmap_delete(map, &vals[i]);
-                assert(v && *v == vals[i]);
-        }) hashmap_free(map);
-
-        map = hashmap_new(sizeof(int), N, seed, seed, hash_int, compare_ints_udata, NULL, NULL);
-        bench("set (cap)", N, {
-                int *v = hashmap_set(map, &vals[i]);
-                assert(!v);
-        }) shuffle(vals, N, sizeof(int));
-        bench("get (cap)", N, {
-                int *v = hashmap_get(map, &vals[i]);
-                assert(v && *v == vals[i]);
-        }) shuffle(vals, N, sizeof(int));
-        bench("delete (cap)",
-              N,
-              {
-                      int *v = hashmap_delete(map, &vals[i]);
-                      assert(v && *v == vals[i]);
-              })
-
-                        hashmap_free(map);
-
-
-        xfree(vals);
-
-        if (total_allocs != 0) {
-                fprintf(stderr, "total_allocs: expected 0, got %lu\n", total_allocs);
-                exit(1);
-        }
-}
-
-int main() {
-        hashmap_set_allocator(xmalloc, xfree);
-
-        if (getenv("BENCH")) {
-                printf("Running hashmap.c benchmarks...\n");
-                benchmarks();
-        } else {
-                printf("Running hashmap.c tests...\n");
-                all();
-                printf("PASSED\n");
-        }
-}
-
-
-#endif
diff --git a/src/libhirte/hashmap/hashmap.h b/src/libhirte/hashmap/hashmap.h
deleted file mode 100644
index 7179d01573..0000000000
--- a/src/libhirte/hashmap/hashmap.h
+++ /dev/null
@@ -1,51 +0,0 @@
-/* SPDX-License-Identifier: MIT */
-// Copyright 2020 Joshua J Baker. All rights reserved.
-// Use of this source code is governed by an MIT-style
-// license that can be found in the LICENSE file.
-
-#pragma once
-
-#include <stdbool.h>
-#include <stddef.h>
-#include <stdint.h>
-
-struct hashmap;
-
-struct hashmap *
-                hashmap_new(size_t elsize,
-                            size_t cap,
-                            uint64_t seed0,
-                            uint64_t seed1,
-                            uint64_t (*hash)(const void *item, uint64_t seed0, uint64_t seed1),
-                            int (*compare)(const void *a, const void *b, void *udata),
-                            void (*elfree)(void *item),
-                            void *udata);
-struct hashmap *hashmap_new_with_allocator(
-                void *(*malloc)(size_t),
-                void *(*realloc)(void *, size_t),
-                void (*free)(void *),
-                size_t elsize,
-                size_t cap,
-                uint64_t seed0,
-                uint64_t seed1,
-                uint64_t (*hash)(const void *item, uint64_t seed0, uint64_t seed1),
-                int (*compare)(const void *a, const void *b, void *udata),
-                void (*elfree)(void *item),
-                void *udata);
-void hashmap_free(struct hashmap *map);
-void hashmap_clear(struct hashmap *map, bool update_cap);
-size_t hashmap_count(struct hashmap *map);
-bool hashmap_oom(struct hashmap *map);
-void *hashmap_get(struct hashmap *map, const void *item);
-void *hashmap_set(struct hashmap *map, const void *item);
-void *hashmap_delete(struct hashmap *map, void *item);
-void *hashmap_probe(struct hashmap *map, uint64_t position);
-bool hashmap_scan(struct hashmap *map, bool (*iter)(const void *item, void *udata), void *udata);
-bool hashmap_iter(struct hashmap *map, size_t *i, void **item);
-
-uint64_t hashmap_sip(const void *data, size_t len, uint64_t seed0, uint64_t seed1);
-uint64_t hashmap_murmur(const void *data, size_t len, uint64_t seed0, uint64_t seed1);
-
-
-// DEPRECATED: use `hashmap_new_with_allocator`
-void hashmap_set_allocator(void *(*malloc)(size_t), void (*free)(void *));
diff --git a/src/libhirte/ini/ini.c b/src/libhirte/ini/ini.c
deleted file mode 100644
index 42125b4faa..0000000000
--- a/src/libhirte/ini/ini.c
+++ /dev/null
@@ -1,312 +0,0 @@
-/* SPDX-License-Identifier: BSD-3-Clause */
-
-/* inih -- simple .INI file parser
-
-The "inih" library is distributed under the New BSD license:
-
-Copyright (c) 2009, Ben Hoyt
-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 Ben Hoyt 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 BEN HOYT ''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 BEN HOYT 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.
-
-(Formatted to follow this project's formatting style)
-*/
-
-#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS)
-#        define _CRT_SECURE_NO_WARNINGS
-#endif
-
-#include <ctype.h>
-#include <stdio.h>
-#include <string.h>
-
-#include "ini.h"
-
-#if !INI_USE_STACK
-#        if INI_CUSTOM_ALLOCATOR
-#                include <stddef.h>
-void *ini_malloc(size_t size);
-void ini_free(void *ptr);
-void *ini_realloc(void *ptr, size_t size);
-#        else
-#                include <stdlib.h>
-#                define ini_malloc malloc
-#                define ini_free free
-#                define ini_realloc realloc
-#        endif
-#endif
-
-#define MAX_SECTION 50
-#define MAX_NAME 50
-
-/* Used by ini_parse_string() to keep track of string parsing state. */
-typedef struct {
-        const char *ptr;
-        size_t num_left;
-} ini_parse_string_ctx;
-
-/* Strip whitespace chars off end of given string, in place. Return s. */
-static char *rstrip(char *s) {
-        char *p = s + strlen(s);
-        while (p > s && isspace((unsigned char) (*--p)))
-                *p = '\0';
-        return s;
-}
-
-/* Return pointer to first non-whitespace char in given string. */
-static char *lskip(const char *s) {
-        while (*s && isspace((unsigned char) (*s)))
-                s++;
-        return (char *) s;
-}
-
-/* Return pointer to first char (of chars) or inline comment in given string,
-   or pointer to NUL at end of string if neither found. Inline comment must
-   be prefixed by a whitespace character to register as a comment. */
-static char *find_chars_or_comment(const char *s, const char *chars) {
-#if INI_ALLOW_INLINE_COMMENTS
-        int was_space = 0;
-        while (*s && (!chars || !strchr(chars, *s)) &&
-               !(was_space && strchr(INI_INLINE_COMMENT_PREFIXES, *s))) {
-                was_space = isspace((unsigned char) (*s));
-                s++;
-        }
-#else
-        while (*s && (!chars || !strchr(chars, *s))) {
-                s++;
-        }
-#endif
-        return (char *) s;
-}
-
-/* Similar to strncpy, but ensures dest (size bytes) is
-   NUL-terminated, and doesn't pad with NULs. */
-static char *strncpy0(char *dest, const char *src, size_t size) {
-        /* Could use strncpy internally, but it causes gcc warnings (see issue #91) */
-        size_t i;
-        for (i = 0; i < size - 1 && src[i]; i++)
-                dest[i] = src[i];
-        dest[i] = '\0';
-        return dest;
-}
-
-/* See documentation in header file. */
-int ini_parse_stream(ini_reader reader, void *stream, ini_handler handler, void *user) {
-        /* Uses a fair bit of stack (use heap instead if you need to) */
-#if INI_USE_STACK
-        char line[INI_MAX_LINE];
-        size_t max_line = INI_MAX_LINE;
-#else
-        char *line;
-        size_t max_line = INI_INITIAL_ALLOC;
-#endif
-#if INI_ALLOW_REALLOC && !INI_USE_STACK
-        char *new_line;
-        size_t offset;
-#endif
-        char section[MAX_SECTION] = "";
-        char prev_name[MAX_NAME] = "";
-
-        char *start;
-        char *end;
-        char *name;
-        char *value;
-        int lineno = 0;
-        int error = 0;
-
-#if !INI_USE_STACK
-        line = (char *) ini_malloc(INI_INITIAL_ALLOC);
-        if (!line) {
-                return -2;
-        }
-#endif
-
-#if INI_HANDLER_LINENO
-#        define HANDLER(u, s, n, v) handler(u, s, n, v, lineno)
-#else
-#        define HANDLER(u, s, n, v) handler(u, s, n, v)
-#endif
-
-        /* Scan through stream line by line */
-        while (reader(line, (int) max_line, stream) != NULL) {
-#if INI_ALLOW_REALLOC && !INI_USE_STACK
-                offset = strlen(line);
-                while (offset == max_line - 1 && line[offset - 1] != '\n') {
-                        max_line *= 2;
-                        if (max_line > INI_MAX_LINE)
-                                max_line = INI_MAX_LINE;
-                        new_line = ini_realloc(line, max_line);
-                        if (!new_line) {
-                                ini_free(line);
-                                return -2;
-                        }
-                        line = new_line;
-                        if (reader(line + offset, (int) (max_line - offset), stream) == NULL)
-                                break;
-                        if (max_line >= INI_MAX_LINE)
-                                break;
-                        offset += strlen(line + offset);
-                }
-#endif
-
-                lineno++;
-
-                start = line;
-#if INI_ALLOW_BOM
-                if (lineno == 1 && (unsigned char) start[0] == 0xEF && (unsigned char) start[1] == 0xBB &&
-                    (unsigned char) start[2] == 0xBF) {
-                        start += 3;
-                }
-#endif
-                start = lskip(rstrip(start));
-
-                if (strchr(INI_START_COMMENT_PREFIXES, *start)) {
-                        /* Start-of-line comment */
-                }
-#if INI_ALLOW_MULTILINE
-                else if (*prev_name && *start && start > line) {
-#        if INI_ALLOW_INLINE_COMMENTS
-                        end = find_chars_or_comment(start, NULL);
-                        if (*end)
-                                *end = '\0';
-                        rstrip(start);
-#        endif
-                        /* Non-blank line with leading whitespace, treat as continuation
-                           of previous name's value (as per Python configparser). */
-                        if (!HANDLER(user, section, prev_name, start) && !error)
-                                error = lineno;
-                }
-#endif
-                else if (*start == '[') {
-                        /* A "[section]" line */
-                        end = find_chars_or_comment(start + 1, "]");
-                        if (*end == ']') {
-                                *end = '\0';
-                                strncpy0(section, start + 1, sizeof(section));
-                                *prev_name = '\0';
-#if INI_CALL_HANDLER_ON_NEW_SECTION
-                                if (!HANDLER(user, section, NULL, NULL) && !error)
-                                        error = lineno;
-#endif
-                        } else if (!error) {
-                                /* No ']' found on section line */
-                                error = lineno;
-                        }
-                } else if (*start) {
-                        /* Not a comment, must be a name[=:]value pair */
-                        end = find_chars_or_comment(start, "=:");
-                        if (*end == '=' || *end == ':') {
-                                *end = '\0';
-                                name = rstrip(start);
-                                value = end + 1;
-#if INI_ALLOW_INLINE_COMMENTS
-                                end = find_chars_or_comment(value, NULL);
-                                if (*end)
-                                        *end = '\0';
-#endif
-                                value = lskip(value);
-                                rstrip(value);
-
-                                /* Valid name[=:]value pair found, call handler */
-                                strncpy0(prev_name, name, sizeof(prev_name));
-                                if (!HANDLER(user, section, name, value) && !error)
-                                        error = lineno;
-                        } else if (!error) {
-                                /* No '=' or ':' found on name[=:]value line */
-#if INI_ALLOW_NO_VALUE
-                                *end = '\0';
-                                name = rstrip(start);
-                                if (!HANDLER(user, section, name, NULL) && !error)
-                                        error = lineno;
-#else
-                                error = lineno;
-#endif
-                        }
-                }
-
-#if INI_STOP_ON_FIRST_ERROR
-                if (error)
-                        break;
-#endif
-        }
-
-#if !INI_USE_STACK
-        ini_free(line);
-#endif
-
-        return error;
-}
-
-/* See documentation in header file. */
-int ini_parse_file(FILE *file, ini_handler handler, void *user) {
-        return ini_parse_stream((ini_reader) fgets, file, handler, user);
-}
-
-/* See documentation in header file. */
-int ini_parse(const char *filename, ini_handler handler, void *user) {
-        FILE *file;
-        int error;
-
-        file = fopen(filename, "r");
-        if (!file)
-                return -1;
-        error = ini_parse_file(file, handler, user);
-        fclose(file);
-        return error;
-}
-
-/* An ini_reader function to read the next line from a string buffer. This
-   is the fgets() equivalent used by ini_parse_string(). */
-static char *ini_reader_string(char *str, int num, void *stream) {
-        ini_parse_string_ctx *ctx = (ini_parse_string_ctx *) stream;
-        const char *ctx_ptr = ctx->ptr;
-        size_t ctx_num_left = ctx->num_left;
-        char *strp = str;
-        char c;
-
-        if (ctx_num_left == 0 || num < 2)
-                return NULL;
-
-        while (num > 1 && ctx_num_left != 0) {
-                c = *ctx_ptr++;
-                ctx_num_left--;
-                *strp++ = c;
-                if (c == '\n')
-                        break;
-                num--;
-        }
-
-        *strp = '\0';
-        ctx->ptr = ctx_ptr;
-        ctx->num_left = ctx_num_left;
-        return str;
-}
-
-/* See documentation in header file. */
-int ini_parse_string(const char *string, ini_handler handler, void *user) {
-        ini_parse_string_ctx ctx;
-
-        ctx.ptr = string;
-        ctx.num_left = strlen(string);
-        return ini_parse_stream((ini_reader) ini_reader_string, &ctx, handler, user);
-}
\ No newline at end of file
diff --git a/src/libhirte/ini/ini.h b/src/libhirte/ini/ini.h
deleted file mode 100644
index 9fac438ed4..0000000000
--- a/src/libhirte/ini/ini.h
+++ /dev/null
@@ -1,158 +0,0 @@
-/* SPDX-License-Identifier: BSD-3-Clause */
-
-#pragma once
-
-/* Make this header file easier to include in C++ code */
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-#include <stdio.h>
-
-/* Nonzero if ini_handler callback should accept lineno parameter. */
-#ifndef INI_HANDLER_LINENO
-#        define INI_HANDLER_LINENO 0
-#endif
-
-/* Visibility symbols, required for Windows DLLs */
-#ifndef INI_API
-#        if defined _WIN32 || defined __CYGWIN__
-#                ifdef INI_SHARED_LIB
-#                        ifdef INI_SHARED_LIB_BUILDING
-#                                define INI_API __declspec(dllexport)
-#                        else
-#                                define INI_API __declspec(dllimport)
-#                        endif
-#                else
-#                        define INI_API
-#                endif
-#        else
-#                if defined(__GNUC__) && __GNUC__ >= 4
-#                        define INI_API __attribute__((visibility("default")))
-#                else
-#                        define INI_API
-#                endif
-#        endif
-#endif
-
-/* Typedef for prototype of handler function. */
-#if INI_HANDLER_LINENO
-typedef int (*ini_handler)(void *user, const char *section, const char *name, const char *value, int lineno);
-#else
-typedef int (*ini_handler)(void *user, const char *section, const char *name, const char *value);
-#endif
-
-/* Typedef for prototype of fgets-style reader function. */
-typedef char *(*ini_reader)(char *str, int num, void *stream);
-
-/* Parse given INI-style file. May have [section]s, name=value pairs
-   (whitespace stripped), and comments starting with ';' (semicolon). Section
-   is "" if name=value pair parsed before any section heading. name:value
-   pairs are also supported as a concession to Python's configparser.
-   For each name=value pair parsed, call handler function with given user
-   pointer as well as section, name, and value (data only valid for duration
-   of handler call). Handler should return nonzero on success, zero on error.
-   Returns 0 on success, line number of first error on parse error (doesn't
-   stop on first error), -1 on file open error, or -2 on memory allocation
-   error (only when INI_USE_STACK is zero).
-*/
-INI_API int ini_parse(const char *filename, ini_handler handler, void *user);
-
-/* Same as ini_parse(), but takes a FILE* instead of filename. This doesn't
-   close the file when it's finished -- the caller must do that. */
-INI_API int ini_parse_file(FILE *file, ini_handler handler, void *user);
-
-/* Same as ini_parse(), but takes an ini_reader function pointer instead of
-   filename. Used for implementing custom or string-based I/O (see also
-   ini_parse_string). */
-INI_API int ini_parse_stream(ini_reader reader, void *stream, ini_handler handler, void *user);
-
-/* Same as ini_parse(), but takes a zero-terminated string with the INI data
-instead of a file. Useful for parsing INI data from a network socket or
-already in memory. */
-INI_API int ini_parse_string(const char *string, ini_handler handler, void *user);
-
-/* Nonzero to allow multi-line value parsing, in the style of Python's
-   configparser. If allowed, ini_parse() will call the handler with the same
-   name for each subsequent line parsed. */
-#ifndef INI_ALLOW_MULTILINE
-#        define INI_ALLOW_MULTILINE 1
-#endif
-
-/* Nonzero to allow a UTF-8 BOM sequence (0xEF 0xBB 0xBF) at the start of
-   the file. See https://github.com/benhoyt/inih/issues/21 */
-#ifndef INI_ALLOW_BOM
-#        define INI_ALLOW_BOM 1
-#endif
-
-/* Chars that begin a start-of-line comment. Per Python configparser, allow
-   both ; and # comments at the start of a line by default. */
-#ifndef INI_START_COMMENT_PREFIXES
-#        define INI_START_COMMENT_PREFIXES ";#"
-#endif
-
-/* Nonzero to allow inline comments (with valid inline comment characters
-   specified by INI_INLINE_COMMENT_PREFIXES). Set to 0 to turn off and match
-   Python 3.2+ configparser behaviour. */
-#ifndef INI_ALLOW_INLINE_COMMENTS
-#        define INI_ALLOW_INLINE_COMMENTS 1
-#endif
-#ifndef INI_INLINE_COMMENT_PREFIXES
-#        define INI_INLINE_COMMENT_PREFIXES ";"
-#endif
-
-/* Nonzero to use stack for line buffer, zero to use heap (malloc/free). */
-#ifndef INI_USE_STACK
-#        define INI_USE_STACK 1
-#endif
-
-/* Maximum line length for any line in INI file (stack or heap). Note that
-   this must be 3 more than the longest line (due to '\r', '\n', and '\0'). */
-#ifndef INI_MAX_LINE
-#        define INI_MAX_LINE 200
-#endif
-
-/* Nonzero to allow heap line buffer to grow via realloc(), zero for a
-   fixed-size buffer of INI_MAX_LINE bytes. Only applies if INI_USE_STACK is
-   zero. */
-#ifndef INI_ALLOW_REALLOC
-#        define INI_ALLOW_REALLOC 0
-#endif
-
-/* Initial size in bytes for heap line buffer. Only applies if INI_USE_STACK
-   is zero. */
-#ifndef INI_INITIAL_ALLOC
-#        define INI_INITIAL_ALLOC 200
-#endif
-
-/* Stop parsing on first error (default is to keep parsing). */
-#ifndef INI_STOP_ON_FIRST_ERROR
-#        define INI_STOP_ON_FIRST_ERROR 0
-#endif
-
-/* Nonzero to call the handler at the start of each new section (with
-   name and value NULL). Default is to only call the handler on
-   each name=value pair. */
-#ifndef INI_CALL_HANDLER_ON_NEW_SECTION
-#        define INI_CALL_HANDLER_ON_NEW_SECTION 0
-#endif
-
-/* Nonzero to allow a name without a value (no '=' or ':' on the line) and
-   call the handler with value NULL in this case. Default is to treat
-   no-value lines as an error. */
-#ifndef INI_ALLOW_NO_VALUE
-#        define INI_ALLOW_NO_VALUE 0
-#endif
-
-/* Nonzero to use custom ini_malloc, ini_free, and ini_realloc memory
-   allocation functions (INI_USE_STACK must also be 0). These functions must
-   have the same signatures as malloc/free/realloc and behave in a similar
-   way. ini_realloc is only needed if INI_ALLOW_REALLOC is set. */
-#ifndef INI_CUSTOM_ALLOCATOR
-#        define INI_CUSTOM_ALLOCATOR 0
-#endif
-
-
-#ifdef __cplusplus
-}
-#endif
diff --git a/src/libhirte/meson.build b/src/libhirte/meson.build
index 817fd76489..5ac8fb8868 100644
--- a/src/libhirte/meson.build
+++ b/src/libhirte/meson.build
@@ -19,10 +19,6 @@ libhirte_src = [
     'common/network.c',
     'common/time-util.c',
     'common/time-util.h',
-    'hashmap/hashmap.c',
-    'hashmap/hashmap.h',
-    'ini/ini.c',
-    'ini/ini.h',
     'service/service.c',
     'service/service.h',
     'service/shutdown.c',
@@ -36,7 +32,9 @@ libhirte_src = [
 hirte_lib = static_library('hirte',
                             libhirte_src,
                             dependencies: [
-                                systemd_dep
+                                systemd_dep,
+                                inih_dep,
+                                hashmapc_dep,
                             ],
                             c_args: common_cflags,
                             include_directories: include_directories('..'))
diff --git a/src/manager/meson.build b/src/manager/meson.build
index 86d36cf733..66f9972d37 100644
--- a/src/manager/meson.build
+++ b/src/manager/meson.build
@@ -23,6 +23,8 @@ executable(
   orch_src,
   dependencies: [
     systemd_dep,
+    inih_dep,
+    hashmapc_dep,
   ],
   link_with: [
     hirte_lib,
diff --git a/src/manager/node.h b/src/manager/node.h
index cb029902a6..ec56e9b5e4 100644
--- a/src/manager/node.h
+++ b/src/manager/node.h
@@ -1,8 +1,9 @@
 /* SPDX-License-Identifier: GPL-2.0-or-later */
 #pragma once
 
+#include <hashmap.h>
+
 #include "libhirte/common/common.h"
-#include "libhirte/hashmap/hashmap.h"
 
 #include "types.h"
 
diff --git a/subprojects/README.md b/subprojects/README.md
new file mode 100644
index 0000000000..0923f11aa9
--- /dev/null
+++ b/subprojects/README.md
@@ -0,0 +1,33 @@
+# Subprojects used by hirte
+
+This directory contains all external subprojects used by hirte as git submodules. These are integrated into the main
+project using mesons [subproject feature](https://mesonbuild.com/Subprojects.html).
+
+Currently, hirte uses the following external projects:
+
+- [hashmap.c](https://github.com/engelmi/hashmap.c.git)
+- [inih](https://github.com/benhoyt/inih.git)
+
+Unfortunately, `hashmap.c` doesn't provide a `meson.build` file and patching on the fly would result in uncommitted
+changes. Because of that a fork has been created containing the necessary `meson.build` and is used for now.
+
+## Adding submodules
+
+New submodules can be added via:
+
+```bash
+git submodule add <repository url> subprojects/<repository name>
+```
+
+After adding the new repository, checkout the desired tag to be used:
+
+```bash
+git -C subprojects/<repository name> checkout <tag>
+```
+
+For the new subproject to be usable by hirte, it needs to be integrated in the main [meson.build](../meson.build). One
+requirement for this is that the subproject itself uses meson and provides a `meson.build`. If the subproject itself is
+a `meson` project, include it as [subproject](https://mesonbuild.com/Subprojects.html). The included `inih` or
+`hashmap.c` can serve as a reference here. Otherwise the `subdir` function can be used (similar to hirte internal projects).
+
+And, finally, add and commit the changes. The same procedure can be used to upgrade a submodule to a newer tag.
diff --git a/subprojects/hashmap.c b/subprojects/hashmap.c
new file mode 160000
index 0000000000..1b0e097150
--- /dev/null
+++ b/subprojects/hashmap.c
@@ -0,0 +1,1 @@
+Subproject commit 1b0e097150aa2ebf68eb54ca807c489dd8502021
diff --git a/subprojects/inih b/subprojects/inih
new file mode 160000
index 0000000000..9cecf0643d
--- /dev/null
+++ b/subprojects/inih
@@ -0,0 +1,1 @@
+Subproject commit 9cecf0643da0846e77f64d10a126d9f48b9e05e8


Test patch:
diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml
index b0bfd3070c..ece543ff63 100644
--- a/.github/workflows/integration-tests.yml
+++ b/.github/workflows/integration-tests.yml
@@ -19,6 +19,7 @@ jobs:
         uses: actions/checkout@v3
         with:
           fetch-depth: 0
+          submodules: 'true'
           # Use githash of a tested commit instead of merge commit
           ref: ${{ github.event.pull_request.head.sha }}
 
@@ -64,6 +65,8 @@ jobs:
 
       - name: Checkout sources
         uses: actions/checkout@v3
+        with:
+          submodules: 'true'
 
       - name: Download artifact
         uses: actions/download-artifact@v3
diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml
index e481a365d3..84a4314700 100644
--- a/.github/workflows/unit-tests.yml
+++ b/.github/workflows/unit-tests.yml
@@ -16,6 +16,8 @@ jobs:
     steps:
       - name: Checkout sources
         uses: actions/checkout@v3
+        with:
+          submodules: 'true'
 
       - name: Building hirte
         run: |
diff --git a/tests/containers/build-base b/tests/containers/build-base
index 748eea2578..4437be9c99 100644
--- a/tests/containers/build-base
+++ b/tests/containers/build-base
@@ -12,6 +12,7 @@ RUN dnf install -y dnf-plugin-config-manager && \
         codespell \
         createrepo_c \
         dnf-utils \
+        g++ \
         gcc \
         git \
         golang-github-cpuguy83-md2man \