[openwrt/openwrt] tools/gnulib: backport patches for gettext

LEDE Commits lede-commits at lists.infradead.org
Sat Jul 26 05:38:57 PDT 2025


robimarko pushed a commit to openwrt/openwrt.git, branch main:
https://git.openwrt.org/1a253a2bb5874f8ed33983216c098b474eda5bca

commit 1a253a2bb5874f8ed33983216c098b474eda5bca
Author: Michael Pratt <mcpratt at pm.me>
AuthorDate: Sat May 31 02:47:11 2025 -0400

    tools/gnulib: backport patches for gettext
    
    The latest versions of gettext rely on several changes to gnulib
    including both changes to modules and new modules
    and some previously gettext specific code being moved to gnulib.
    
    Backport these changes in order to allow updating gettext
    while using the local gnulib copy of sources.
    
    Add patch:
     - 640-mem-hash-map.patch
     - 645-next-prime.patch
     - 646-hashcode-string.patch
     - 647-hashkey-string.patch
     - 650-package-version.patch
     - 651-package-version-simplify.patch
     - 652-package-version-simplify-further.patch
     - 653-package-version-warning.patch
     - 660-version-stamp.patch
     - 689-vc-mtime.patch
     - 755-clean-temp-hashkey.patch
     - 795-string-desc-rename-functions.patch
     - 796-vc-mtime-less-read.patch
     - 797-vc-mtime-add-api.patch
     - 798-vc-mtime-add-api.patch
     - 799-vc-mtime-old-git.patch
     - 900-str_startswith-module.patch
     - 901-str_endswith-module.patch
    
    Signed-off-by: Michael Pratt <mcpratt at pm.me>
    Link: https://github.com/openwrt/openwrt/pull/16522
    Signed-off-by: Robert Marko <robimarko at gmail.com>
---
 tools/gnulib/patches/640-mem-hash-map.patch        |  494 +++++++++
 tools/gnulib/patches/645-next-prime.patch          |  218 ++++
 tools/gnulib/patches/646-hashcode-string.patch     |  294 +++++
 tools/gnulib/patches/647-hashkey-string.patch      |  215 ++++
 tools/gnulib/patches/650-package-version.patch     |  176 +++
 .../patches/651-package-version-simplify.patch     |   66 ++
 .../652-package-version-simplify-further.patch     |   89 ++
 .../patches/653-package-version-warning.patch      |   32 +
 tools/gnulib/patches/660-version-stamp.patch       |   75 ++
 tools/gnulib/patches/689-vc-mtime.patch            |  366 +++++++
 tools/gnulib/patches/755-clean-temp-hashkey.patch  |   64 ++
 .../patches/795-string-desc-rename-functions.patch | 1137 ++++++++++++++++++++
 tools/gnulib/patches/796-vc-mtime-less-read.patch  |   44 +
 tools/gnulib/patches/797-vc-mtime-add-api.patch    |  968 +++++++++++++++++
 tools/gnulib/patches/798-vc-mtime-add-api.patch    |   91 ++
 tools/gnulib/patches/799-vc-mtime-old-git.patch    |  125 +++
 .../gnulib/patches/900-str_startswith-module.patch |  117 ++
 tools/gnulib/patches/901-str_endswith-module.patch |  119 ++
 18 files changed, 4690 insertions(+)

diff --git a/tools/gnulib/patches/640-mem-hash-map.patch b/tools/gnulib/patches/640-mem-hash-map.patch
new file mode 100644
index 0000000000..aaebe545fa
--- /dev/null
+++ b/tools/gnulib/patches/640-mem-hash-map.patch
@@ -0,0 +1,494 @@
+From 5a842672e79a7a5f6be837c483be4f9901a4ecc0 Mon Sep 17 00:00:00 2001
+From: Bruno Haible <bruno at clisp.org>
+Date: Wed, 30 Apr 2025 03:19:10 +0200
+Subject: [PATCH] New module mem-hash-map.
+
+* lib/mem-hash-map.h: New file, from GNU gettext.
+* lib/mem-hash-map.c: New file, from GNU gettext.
+* modules/mem-hash-map: New file, from GNU gettext.
+---
+ ChangeLog            |   7 +
+ lib/mem-hash-map.c   | 352 +++++++++++++++++++++++++++++++++++++++++++
+ lib/mem-hash-map.h   |  90 +++++++++++
+ modules/mem-hash-map |  25 +++
+ 4 files changed, 474 insertions(+)
+ create mode 100644 lib/mem-hash-map.c
+ create mode 100644 lib/mem-hash-map.h
+ create mode 100644 modules/mem-hash-map
+
+--- /dev/null
++++ b/lib/mem-hash-map.c
+@@ -0,0 +1,352 @@
++/* Simple hash table (no removals) where the keys are memory blocks.
++   Copyright (C) 1994-2025 Free Software Foundation, Inc.
++   Written by Ulrich Drepper <drepper at gnu.ai.mit.edu>, October 1994.
++
++   This file is free software: you can redistribute it and/or modify
++   it under the terms of the GNU General Public License as published
++   by the Free Software Foundation, either version 3 of the License,
++   or (at your option) any later version.
++
++   This file is distributed in the hope that it will be useful,
++   but WITHOUT ANY WARRANTY; without even the implied warranty of
++   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
++   GNU General Public License for more details.
++
++   You should have received a copy of the GNU General Public License
++   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
++
++#include <config.h>
++
++/* Specification.  */
++#include "mem-hash-map.h"
++
++#include <stdlib.h>
++#include <string.h>
++#include <stdio.h>
++#include <limits.h>
++#include <sys/types.h>
++
++#include "next-prime.h"
++
++/* Since this simple implementation of hash tables allows only insertion, no
++   removal of entries, the right data structure for the memory holding all keys
++   is an obstack.  */
++#include "obstack.h"
++
++/* Use checked memory allocation.  */
++#include "xalloc.h"
++
++#define obstack_chunk_alloc xmalloc
++#define obstack_chunk_free free
++
++
++typedef struct hash_entry
++{
++  size_t used;         /* Hash code of the key, or 0 for an unused entry.  */
++  const void *key;     /* Key.  */
++  size_t keylen;
++  void *data;          /* Value.  */
++  struct hash_entry *next;
++}
++hash_entry;
++
++
++/* Initialize a hash table.  INIT_SIZE > 1 is the initial number of available
++   entries.
++   Return 0 always.  */
++int
++hash_init (hash_table *htab, size_t init_size)
++{
++  /* We need the size to be a prime.  */
++  init_size = next_prime (init_size);
++
++  /* Initialize the data structure.  */
++  htab->size = init_size;
++  htab->filled = 0;
++  htab->first = NULL;
++  htab->table = XCALLOC (init_size + 1, hash_entry);
++
++  obstack_init (&htab->mem_pool);
++
++  return 0;
++}
++
++
++/* Delete a hash table's contents.
++   Return 0 always.  */
++int
++hash_destroy (hash_table *htab)
++{
++  free (htab->table);
++  obstack_free (&htab->mem_pool, NULL);
++  return 0;
++}
++
++
++/* Compute a hash code for a key consisting of KEYLEN bytes starting at KEY
++   in memory.  */
++static size_t
++compute_hashval (const void *key, size_t keylen)
++{
++  size_t cnt;
++  size_t hval;
++
++  /* Compute the hash value for the given string.  The algorithm
++     is taken from [Aho,Sethi,Ullman], fixed according to
++     https://haible.de/bruno/hashfunc.html.  */
++  cnt = 0;
++  hval = keylen;
++  while (cnt < keylen)
++    {
++      hval = (hval << 9) | (hval >> (sizeof (size_t) * CHAR_BIT - 9));
++      hval += (size_t) *(((const char *) key) + cnt++);
++    }
++  return hval != 0 ? hval : ~((size_t) 0);
++}
++
++
++/* References:
++   [Aho,Sethi,Ullman] Compilers: Principles, Techniques and Tools, 1986
++   [Knuth]            The Art of Computer Programming, part3 (6.4) */
++
++/* Look up a given key in the hash table.
++   Return the index of the entry, if present, or otherwise the index a free
++   entry where it could be inserted.  */
++static size_t
++lookup (const hash_table *htab,
++        const void *key, size_t keylen,
++        size_t hval)
++{
++  size_t hash;
++  size_t idx;
++  hash_entry *table = htab->table;
++
++  /* First hash function: simply take the modul but prevent zero.  */
++  hash = 1 + hval % htab->size;
++
++  idx = hash;
++
++  if (table[idx].used)
++    {
++      if (table[idx].used == hval && table[idx].keylen == keylen
++          && memcmp (table[idx].key, key, keylen) == 0)
++        return idx;
++
++      /* Second hash function as suggested in [Knuth].  */
++      hash = 1 + hval % (htab->size - 2);
++
++      do
++        {
++          if (idx <= hash)
++            idx = htab->size + idx - hash;
++          else
++            idx -= hash;
++
++          /* If entry is found use it.  */
++          if (table[idx].used == hval && table[idx].keylen == keylen
++              && memcmp (table[idx].key, key, keylen) == 0)
++            return idx;
++        }
++      while (table[idx].used);
++    }
++  return idx;
++}
++
++
++/* Look up the value of a key in the given table.
++   If found, return 0 and set *RESULT to it.  Otherwise return -1.  */
++int
++hash_find_entry (const hash_table *htab, const void *key, size_t keylen,
++                 void **result)
++{
++  hash_entry *table = htab->table;
++  size_t idx = lookup (htab, key, keylen, compute_hashval (key, keylen));
++
++  if (table[idx].used == 0)
++    return -1;
++
++  *result = table[idx].data;
++  return 0;
++}
++
++
++/* Insert the pair (KEY[0..KEYLEN-1], DATA) in the hash table at index IDX.
++   HVAL is the key's hash code.  IDX depends on it.  The table entry at index
++   IDX is known to be unused.  */
++static void
++insert_entry_2 (hash_table *htab,
++                const void *key, size_t keylen,
++                size_t hval, size_t idx, void *data)
++{
++  hash_entry *table = htab->table;
++
++  table[idx].used = hval;
++  table[idx].key = key;
++  table[idx].keylen = keylen;
++  table[idx].data = data;
++
++  /* List the new value in the list.  */
++  if (htab->first == NULL)
++    {
++      table[idx].next = &table[idx];
++      htab->first = &table[idx];
++    }
++  else
++    {
++      table[idx].next = htab->first->next;
++      htab->first->next = &table[idx];
++      htab->first = &table[idx];
++    }
++
++  ++htab->filled;
++}
++
++
++/* Grow the hash table.  */
++static void
++resize (hash_table *htab)
++{
++  size_t old_size = htab->size;
++  hash_entry *table = htab->table;
++  size_t idx;
++
++  htab->size = next_prime (htab->size * 2);
++  htab->filled = 0;
++  htab->first = NULL;
++  htab->table = XCALLOC (1 + htab->size, hash_entry);
++
++  for (idx = 1; idx <= old_size; ++idx)
++    if (table[idx].used)
++      insert_entry_2 (htab, table[idx].key, table[idx].keylen,
++                      table[idx].used,
++                      lookup (htab, table[idx].key, table[idx].keylen,
++                              table[idx].used),
++                      table[idx].data);
++
++  free (table);
++}
++
++
++/* Try to insert the pair (KEY[0..KEYLEN-1], DATA) in the hash table.
++   Return non-NULL (more precisely, the address of the KEY inside the table's
++   memory pool) if successful, or NULL if there is already an entry with the
++   given key.  */
++const void *
++hash_insert_entry (hash_table *htab,
++                   const void *key, size_t keylen,
++                   void *data)
++{
++  size_t hval = compute_hashval (key, keylen);
++  hash_entry *table = htab->table;
++  size_t idx = lookup (htab, key, keylen, hval);
++
++  if (table[idx].used)
++    /* We don't want to overwrite the old value.  */
++    return NULL;
++  else
++    {
++      /* An empty bucket has been found.  */
++      void *keycopy = obstack_copy (&htab->mem_pool, key, keylen);
++      insert_entry_2 (htab, keycopy, keylen, hval, idx, data);
++      if (100 * htab->filled > 75 * htab->size)
++        /* Table is filled more than 75%.  Resize the table.  */
++        resize (htab);
++      return keycopy;
++    }
++}
++
++
++/* Insert the pair (KEY[0..KEYLEN-1], DATA) in the hash table.
++   Return 0.  */
++int
++hash_set_value (hash_table *htab,
++                const void *key, size_t keylen,
++                void *data)
++{
++  size_t hval = compute_hashval (key, keylen);
++  hash_entry *table = htab->table;
++  size_t idx = lookup (htab, key, keylen, hval);
++
++  if (table[idx].used)
++    {
++      /* Overwrite the old value.  */
++      table[idx].data = data;
++      return 0;
++    }
++  else
++    {
++      /* An empty bucket has been found.  */
++      void *keycopy = obstack_copy (&htab->mem_pool, key, keylen);
++      insert_entry_2 (htab, keycopy, keylen, hval, idx, data);
++      if (100 * htab->filled > 75 * htab->size)
++        /* Table is filled more than 75%.  Resize the table.  */
++        resize (htab);
++      return 0;
++    }
++}
++
++
++/* Steps *PTR forward to the next used entry in the given hash table.  *PTR
++   should be initially set to NULL.  Store information about the next entry
++   in *KEY, *KEYLEN, *DATA.
++   Return 0 normally, -1 when the whole hash table has been traversed.  */
++int
++hash_iterate (hash_table *htab, void **ptr, const void **key, size_t *keylen,
++              void **data)
++{
++  hash_entry *curr;
++
++  if (*ptr == NULL)
++    {
++      if (htab->first == NULL)
++        return -1;
++      curr = htab->first;
++    }
++  else
++    {
++      if (*ptr == htab->first)
++        return -1;
++      curr = (hash_entry *) *ptr;
++    }
++  curr = curr->next;
++  *ptr = (void *) curr;
++
++  *key = curr->key;
++  *keylen = curr->keylen;
++  *data = curr->data;
++  return 0;
++}
++
++
++/* Steps *PTR forward to the next used entry in the given hash table.  *PTR
++   should be initially set to NULL.  Store information about the next entry
++   in *KEY, *KEYLEN, *DATAP.  *DATAP is set to point to the storage of the
++   value; modifying **DATAP will modify the value of the entry.
++   Return 0 normally, -1 when the whole hash table has been traversed.  */
++int
++hash_iterate_modify (hash_table *htab, void **ptr,
++                     const void **key, size_t *keylen,
++                     void ***datap)
++{
++  hash_entry *curr;
++
++  if (*ptr == NULL)
++    {
++      if (htab->first == NULL)
++        return -1;
++      curr = htab->first;
++    }
++  else
++    {
++      if (*ptr == htab->first)
++        return -1;
++      curr = (hash_entry *) *ptr;
++    }
++  curr = curr->next;
++  *ptr = (void *) curr;
++
++  *key = curr->key;
++  *keylen = curr->keylen;
++  *datap = &curr->data;
++  return 0;
++}
+--- /dev/null
++++ b/lib/mem-hash-map.h
+@@ -0,0 +1,90 @@
++/* Simple hash table (no removals) where the keys are memory blocks.
++   Copyright (C) 1995-2025 Free Software Foundation, Inc.
++
++   This file is free software: you can redistribute it and/or modify
++   it under the terms of the GNU General Public License as published
++   by the Free Software Foundation, either version 3 of the License,
++   or (at your option) any later version.
++
++   This file is distributed in the hope that it will be useful,
++   but WITHOUT ANY WARRANTY; without even the implied warranty of
++   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
++   GNU General Public License for more details.
++
++   You should have received a copy of the GNU General Public License
++   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
++
++#ifndef _GL_MEM_HASH_MAP_H
++#define _GL_MEM_HASH_MAP_H
++
++#include <stddef.h>
++
++#include "obstack.h"
++
++#ifdef __cplusplus
++extern "C" {
++#endif
++
++struct hash_entry;
++
++typedef struct hash_table
++{
++  size_t size;              /* Number of allocated entries.  */
++  size_t filled;            /* Number of used entries.  */
++  struct hash_entry *first; /* Pointer to head of list of entries.  */
++  struct hash_entry *table; /* Pointer to array of entries.  */
++  struct obstack mem_pool;  /* Memory pool holding the keys.  */
++}
++hash_table;
++
++/* Initialize a hash table.  INIT_SIZE > 1 is the initial number of available
++   entries.
++   Return 0 always.  */
++extern int hash_init (hash_table *htab, size_t init_size);
++
++/* Delete a hash table's contents.
++   Return 0 always.  */
++extern int hash_destroy (hash_table *htab);
++
++/* Look up the value of a key in the given table.
++   If found, return 0 and set *RESULT to it.  Otherwise return -1.  */
++extern int hash_find_entry (const hash_table *htab,
++                            const void *key, size_t keylen,
++                            void **result);
++
++/* Try to insert the pair (KEY[0..KEYLEN-1], DATA) in the hash table.
++   Return non-NULL (more precisely, the address of the KEY inside the table's
++   memory pool) if successful, or NULL if there is already an entry with the
++   given key.  */
++extern const void * hash_insert_entry (hash_table *htab,
++                                       const void *key, size_t keylen,
++                                       void *data);
++
++/* Insert the pair (KEY[0..KEYLEN-1], DATA) in the hash table.
++   Return 0.  */
++extern int hash_set_value (hash_table *htab,
++                           const void *key, size_t keylen,
++                           void *data);
++
++/* Steps *PTR forward to the next used entry in the given hash table.  *PTR
++   should be initially set to NULL.  Store information about the next entry
++   in *KEY, *KEYLEN, *DATA.
++   Return 0 normally, -1 when the whole hash table has been traversed.  */
++extern int hash_iterate (hash_table *htab, void **ptr,
++                         const void **key, size_t *keylen,
++                         void **data);
++
++/* Steps *PTR forward to the next used entry in the given hash table.  *PTR
++   should be initially set to NULL.  Store information about the next entry
++   in *KEY, *KEYLEN, *DATAP.  *DATAP is set to point to the storage of the
++   value; modifying **DATAP will modify the value of the entry.
++   Return 0 normally, -1 when the whole hash table has been traversed.  */
++extern int hash_iterate_modify (hash_table *htab, void **ptr,
++                                const void **key, size_t *keylen,
++                                void ***datap);
++
++#ifdef __cplusplus
++}
++#endif
++
++#endif /* not _GL_MEM_HASH_MAP_H */
+--- /dev/null
++++ b/modules/mem-hash-map
+@@ -0,0 +1,25 @@
++Description:
++Simple hash table (no removals) where the keys are memory blocks.
++
++Files:
++lib/mem-hash-map.h
++lib/mem-hash-map.c
++
++Depends-on:
++next-prime
++obstack
++xalloc
++
++configure.ac:
++
++Makefile.am:
++lib_SOURCES += mem-hash-map.h mem-hash-map.c
++
++Include:
++"mem-hash-map.h"
++
++License:
++GPL
++
++Maintainer:
++Bruno Haible
diff --git a/tools/gnulib/patches/645-next-prime.patch b/tools/gnulib/patches/645-next-prime.patch
new file mode 100644
index 0000000000..66f482f5ec
--- /dev/null
+++ b/tools/gnulib/patches/645-next-prime.patch
@@ -0,0 +1,218 @@
+From 0b953ba82830f51ce9b939700705d238f9b0c0ba Mon Sep 17 00:00:00 2001
+From: Bruno Haible <bruno at clisp.org>
+Date: Wed, 30 Apr 2025 01:52:17 +0200
+Subject: [PATCH] New module next-prime.
+
+* lib/next-prime.h: New file, based on lib/hash.c.
+* lib/next-prime.c: New file, based on lib/hash.c.
+* modules/next-prime: New file.
+* lib/hash.c: Include next-prime.h.
+(is_prime, next_prime): Remove functions.
+* modules/hash (Depends-on): Add next-prime.
+---
+ ChangeLog          | 10 +++++++++
+ lib/hash.c         | 39 +-------------------------------
+ lib/next-prime.c   | 56 ++++++++++++++++++++++++++++++++++++++++++++++
+ lib/next-prime.h   | 41 +++++++++++++++++++++++++++++++++
+ modules/hash       |  1 +
+ modules/next-prime | 24 ++++++++++++++++++++
+ 6 files changed, 133 insertions(+), 38 deletions(-)
+ create mode 100644 lib/next-prime.c
+ create mode 100644 lib/next-prime.h
+ create mode 100644 modules/next-prime
+
+--- a/lib/hash.c
++++ b/lib/hash.c
+@@ -27,6 +27,7 @@
+ #include "hash.h"
+ 
+ #include "bitrotate.h"
++#include "next-prime.h"
+ #include "xalloc-oversized.h"
+ 
+ #include <errno.h>
+@@ -390,44 +391,6 @@ hash_string (const char *string, size_t
+ 
+ #endif /* not USE_DIFF_HASH */
+ 
+-/* Return true if CANDIDATE is a prime number.  CANDIDATE should be an odd
+-   number at least equal to 11.  */
+-
+-static bool _GL_ATTRIBUTE_CONST
+-is_prime (size_t candidate)
+-{
+-  size_t divisor = 3;
+-  size_t square = divisor * divisor;
+-
+-  while (square < candidate && (candidate % divisor))
+-    {
+-      divisor++;
+-      square += 4 * divisor;
+-      divisor++;
+-    }
+-
+-  return (candidate % divisor ? true : false);
+-}
+-
+-/* Round a given CANDIDATE number up to the nearest prime, and return that
+-   prime.  Primes lower than 10 are merely skipped.  */
+-
+-static size_t _GL_ATTRIBUTE_CONST
+-next_prime (size_t candidate)
+-{
+-  /* Skip small primes.  */
+-  if (candidate < 10)
+-    candidate = 10;
+-
+-  /* Make it definitely odd.  */
+-  candidate |= 1;
+-
+-  while (SIZE_MAX != candidate && !is_prime (candidate))
+-    candidate += 2;
+-
+-  return candidate;
+-}
+-
+ void
+ hash_reset_tuning (Hash_tuning *tuning)
+ {
+--- /dev/null
++++ b/lib/next-prime.c
+@@ -0,0 +1,56 @@
++/* Finding the next prime >= a given small integer.
++   Copyright (C) 1995-2025 Free Software Foundation, Inc.
++
++   This file is free software: you can redistribute it and/or modify
++   it under the terms of the GNU Lesser General Public License as
++   published by the Free Software Foundation; either version 2.1 of the
++   License, or (at your option) any later version.
++
++   This file is distributed in the hope that it will be useful,
++   but WITHOUT ANY WARRANTY; without even the implied warranty of
++   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
++   GNU Lesser General Public License for more details.
++
++   You should have received a copy of the GNU Lesser General Public License
++   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
++
++#include <config.h>
++
++/* Specification.  */
++#include "next-prime.h"
++
++#include <stdint.h> /* for SIZE_MAX */
++
++/* Return true if CANDIDATE is a prime number.  CANDIDATE should be an odd
++   number at least equal to 11.  */
++static bool _GL_ATTRIBUTE_CONST
++is_prime (size_t candidate)
++{
++  size_t divisor = 3;
++  size_t square = divisor * divisor;
++
++  while (square < candidate && (candidate % divisor))
++    {
++      divisor++;
++      square += 4 * divisor;
++      divisor++;
++    }
++
++  return (candidate % divisor ? true : false);
++}
++
++size_t _GL_ATTRIBUTE_CONST
++next_prime (size_t candidate)
++{
++  /* Skip small primes.  */
++  if (candidate < 10)
++    candidate = 10;
++
++  /* Make it definitely odd.  */
++  candidate |= 1;
++
++  while (SIZE_MAX != candidate && !is_prime (candidate))
++    candidate += 2;
++
++  return candidate;
++}
+--- /dev/null
++++ b/lib/next-prime.h
+@@ -0,0 +1,41 @@
++/* Finding the next prime >= a given small integer.
++   Copyright (C) 1995-2025 Free Software Foundation, Inc.
++
++   This file is free software: you can redistribute it and/or modify
++   it under the terms of the GNU Lesser General Public License as
++   published by the Free Software Foundation; either version 2.1 of the
++   License, or (at your option) any later version.
++
++   This file is distributed in the hope that it will be useful,
++   but WITHOUT ANY WARRANTY; without even the implied warranty of
++   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
++   GNU Lesser General Public License for more details.
++
++   You should have received a copy of the GNU Lesser General Public License
++   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
++
++#ifndef _GL_NEXT_PRIME_H
++#define _GL_NEXT_PRIME_H
++
++/* This file uses _GL_ATTRIBUTE_CONST.  */
++#if !_GL_CONFIG_H_INCLUDED
++ #error "Please include config.h first."
++#endif
++
++#include <stddef.h>
++
++#ifdef __cplusplus
++extern "C" {
++#endif
++
++
++/* Round a given CANDIDATE number up to the nearest prime, and return that
++   prime.  Primes lower than 10 are merely skipped.  */
++extern size_t _GL_ATTRIBUTE_CONST next_prime (size_t candidate);
++
++
++#ifdef __cplusplus
++}
++#endif
++
++#endif /* _GL_NEXT_PRIME_H */
+--- a/modules/hash
++++ b/modules/hash
+@@ -10,6 +10,7 @@ bitrotate
+ calloc-posix
+ free-posix
+ malloc-posix
++next-prime
+ bool
+ stdint-h
+ xalloc-oversized
+--- /dev/null
++++ b/modules/next-prime
+@@ -0,0 +1,24 @@
++Description:
++Finding the next prime >= a given small integer.
++
++Files:
++lib/next-prime.h
++lib/next-prime.c
++
++Depends-on:
++bool
++stdint-h
++
++configure.ac:
++
++Makefile.am:
++lib_SOURCES += next-prime.h next-prime.c
++
++Include:
++"next-prime.h"
++
++License:
++LGPLv2+
++
++Maintainer:
++all
diff --git a/tools/gnulib/patches/646-hashcode-string.patch b/tools/gnulib/patches/646-hashcode-string.patch
new file mode 100644
index 0000000000..ee2ebd237f
--- /dev/null
+++ b/tools/gnulib/patches/646-hashcode-string.patch
@@ -0,0 +1,294 @@
+From 64042bb91aea5f854ca8a8938e2b3f7d1935e4f1 Mon Sep 17 00:00:00 2001
+From: Bruno Haible <bruno at clisp.org>
+Date: Wed, 30 Apr 2025 12:47:37 +0200
+Subject: [PATCH] New module hashcode-string1.
+
+* lib/hashcode-string1.h: New file.
+* lib/hashcode-string1.c: New file, based on lib/hash.c.
+* modules/hashcode-string1: New file.
+* lib/hash.h: Include hashcode-string1.h.
+(hash_string): Remove declaration.
+* lib/hash.c (hash_string): Remove function.
+* modules/hash (Depends-on): Add hashcode-string1.
+* lib/exclude.c: Include hashcode-string1.h.
+* modules/exclude (Depends-on): Add hashcode-string1.
+---
+ ChangeLog                | 13 +++++++++
+ lib/exclude.c            |  1 +
+ lib/hash.c               | 59 ++++++--------------------------------
+ lib/hash.h               | 11 +++----
+ lib/hashcode-string1.c   | 62 ++++++++++++++++++++++++++++++++++++++++
+ lib/hashcode-string1.h   | 38 ++++++++++++++++++++++++
+ modules/exclude          |  1 +
+ modules/hash             |  1 +
+ modules/hashcode-string1 | 24 ++++++++++++++++
+ 9 files changed, 154 insertions(+), 56 deletions(-)
+ create mode 100644 lib/hashcode-string1.c
+ create mode 100644 lib/hashcode-string1.h
+ create mode 100644 modules/hashcode-string1
+
+--- a/lib/exclude.c
++++ b/lib/exclude.c
+@@ -36,6 +36,7 @@
+ #include "filename.h"
+ #include <fnmatch.h>
+ #include "hash.h"
++#include "hashcode-string1.h"
+ #if GNULIB_MCEL_PREFER
+ # include "mcel.h"
+ #else
+--- a/lib/hash.c
++++ b/lib/hash.c
+@@ -345,57 +345,6 @@ hash_do_for_each (const Hash_table *tabl
+   return counter;
+ }
+ 
+-/* Allocation and clean-up.  */
+-
+-#if USE_DIFF_HASH
+-
+-/* About hashings, Paul Eggert writes to me (FP), on 1994-01-01: "Please see
+-   B. J. McKenzie, R. Harries & T. Bell, Selecting a hashing algorithm,
+-   Software--practice & experience 20, 2 (Feb 1990), 209-224.  Good hash
+-   algorithms tend to be domain-specific, so what's good for [diffutils'] io.c
+-   may not be good for your application."  */
+-
+-size_t
+-hash_string (const char *string, size_t n_buckets)
+-{
+-# define HASH_ONE_CHAR(Value, Byte) \
+-  ((Byte) + rotl_sz (Value, 7))
+-
+-  size_t value = 0;
+-  unsigned char ch;
+-
+-  for (; (ch = *string); string++)
+-    value = HASH_ONE_CHAR (value, ch);
+-  return value % n_buckets;
+-
+-# undef HASH_ONE_CHAR
+-}
+-
+-#else /* not USE_DIFF_HASH */
+-
+-/* This one comes from 'recode', and performs a bit better than the above as
+-   per a few experiments.  It is inspired from a hashing routine found in the
+-   very old Cyber 'snoop', itself written in typical Greg Mansfield style.
+-   (By the way, what happened to this excellent man?  Is he still alive?)  */
+-
+-size_t
+-hash_string (const char *string, size_t n_buckets)
+-{
+-  size_t value = 0;
+-  unsigned char ch;
+-
+-  for (; (ch = *string); string++)
+-    value = (value * 31 + ch) % n_buckets;
+-  return value;
+-}
+-
+-#endif /* not USE_DIFF_HASH */
+-
+-void
+-hash_reset_tuning (Hash_tuning *tuning)
+-{
+-  *tuning = default_tuning;
+-}
+ 
+ /* If the user passes a NULL hasher, we hash the raw pointer.  */
+ static size_t
+@@ -418,6 +367,14 @@ raw_comparator (const void *a, const voi
+ }
+ 
+ 
++/* Allocation and clean-up.  */
++
++void
++hash_reset_tuning (Hash_tuning *tuning)
++{
++  *tuning = default_tuning;
++}
++
+ /* For the given hash TABLE, check the user supplied tuning structure for
+    reasonable values, and return true if there is no gross error with it.
+    Otherwise, definitively reset the TUNING field to some acceptable default
+--- a/lib/hash.h
++++ b/lib/hash.h
+@@ -134,11 +134,6 @@ extern size_t hash_do_for_each (const Ha
+  * Allocation and clean-up.
+  */
+ 
+-/* Return a hash index for a NUL-terminated STRING between 0 and N_BUCKETS-1.
+-   This is a convenience routine for constructing other hashing functions.  */
+-extern size_t hash_string (const char *string, size_t n_buckets)
+-       _GL_ATTRIBUTE_PURE;
+-
+ extern void hash_reset_tuning (Hash_tuning *tuning);
+ 
+ typedef size_t (*Hash_hasher) (const void *entry, size_t table_size);
+@@ -266,6 +261,12 @@ extern void *hash_remove (Hash_table *ta
+ _GL_ATTRIBUTE_DEPRECATED
+ extern void *hash_delete (Hash_table *table, const void *entry);
+ 
++
++# if GNULIB_HASHCODE_STRING1
++/* Include declarations of module 'hashcode-string1'.  */
++#  include "hashcode-string1.h"
++# endif
++
+ # ifdef __cplusplus
+ }
+ # endif
+--- /dev/null
++++ b/lib/hashcode-string1.c
+@@ -0,0 +1,62 @@
++/* hashcode-string1.c -- compute a hash value from a NUL-terminated string.
++
++   Copyright (C) 1998-2004, 2006-2007, 2009-2025 Free Software Foundation, Inc.
++
++   This file is free software: you can redistribute it and/or modify
++   it under the terms of the GNU Lesser General Public License as
++   published by the Free Software Foundation; either version 2.1 of the
++   License, or (at your option) any later version.
++
++   This file is distributed in the hope that it will be useful,
++   but WITHOUT ANY WARRANTY; without even the implied warranty of
++   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
++   GNU Lesser General Public License for more details.
++
++   You should have received a copy of the GNU Lesser General Public License
++   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
++
++#include <config.h>
++
++/* Specification.  */
++#include "hashcode-string1.h"
++
++#if USE_DIFF_HASH
++
++# include "bitrotate.h"
++
++/* About hashings, Paul Eggert writes to me (FP), on 1994-01-01: "Please see
++   B. J. McKenzie, R. Harries & T. Bell, Selecting a hashing algorithm,
++   Software--practice & experience 20, 2 (Feb 1990), 209-224.  Good hash
++   algorithms tend to be domain-specific, so what's good for [diffutils'] io.c
++   may not be good for your application."  */
++
++size_t
++hash_string (const char *string, size_t tablesize)
++{
++  size_t value = 0;
++  unsigned char ch;
++
++  for (; (ch = *string); string++)
++    value = ch + rotl_sz (value, 7);
++  return value % tablesize;
++}
++
++#else /* not USE_DIFF_HASH */
++
++/* This one comes from 'recode', and performs a bit better than the above as
++   per a few experiments.  It is inspired from a hashing routine found in the
++   very old Cyber 'snoop', itself written in typical Greg Mansfield style.
++   (By the way, what happened to this excellent man?  Is he still alive?)  */
++
++size_t
++hash_string (const char *string, size_t tablesize)
++{
++  size_t value = 0;
++  unsigned char ch;
++
++  for (; (ch = *string); string++)
++    value = (value * 31 + ch) % tablesize;
++  return value;
++}
++
++#endif /* not USE_DIFF_HASH */
+--- /dev/null
++++ b/lib/hashcode-string1.h
+@@ -0,0 +1,38 @@
++/* hashcode-string1.h -- declaration for a simple hash function
++   Copyright (C) 1998-2004, 2006-2007, 2009-2025 Free Software Foundation, Inc.
++
++   This file is free software: you can redistribute it and/or modify
++   it under the terms of the GNU Lesser General Public License as
++   published by the Free Software Foundation; either version 2.1 of the
++   License, or (at your option) any later version.
++
++   This file is distributed in the hope that it will be useful,
++   but WITHOUT ANY WARRANTY; without even the implied warranty of
++   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
++   GNU Lesser General Public License for more details.
++
++   You should have received a copy of the GNU Lesser General Public License
++   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
++
++/* This file uses _GL_ATTRIBUTE_PURE.  */
++#if !_GL_CONFIG_H_INCLUDED
++ #error "Please include config.h first."
++#endif
++
++#include <stddef.h>
++
++#ifdef __cplusplus
++extern "C" {
++#endif
++
++
++/* Compute a hash code for a NUL-terminated string S,
++   and return the hash code modulo TABLESIZE.
++   The result is platform dependent: it depends on the size of the 'size_t'
++   type.  */
++extern size_t hash_string (char const *s, size_t tablesize) _GL_ATTRIBUTE_PURE;
++
++
++#ifdef __cplusplus
++}
++#endif
+--- a/modules/exclude
++++ b/modules/exclude
+@@ -12,6 +12,7 @@ filename
+ fnmatch
+ fopen-gnu
+ hash
++hashcode-string1
+ mbscasecmp
+ mbuiter               [test "$GNULIB_MCEL_PREFER" != yes]
+ nullptr
+--- a/modules/hash
++++ b/modules/hash
+@@ -14,6 +14,7 @@ next-prime
+ bool
+ stdint-h
+ xalloc-oversized
++hashcode-string1
+ 
+ configure.ac:
+ 
+--- /dev/null
++++ b/modules/hashcode-string1
+@@ -0,0 +1,24 @@
++Description:
++Compute a hash value for a NUL-terminated string.
++
++Files:
++lib/hashcode-string1.h
++lib/hashcode-string1.c
++
++Depends-on:
++bitrotate
++
++configure.ac:
++gl_MODULE_INDICATOR([hashcode-string1])
++
++Makefile.am:
++lib_SOURCES += hashcode-string1.h hashcode-string1.c
++
++Include:
++"hashcode-string1.h"
++
++License:
++LGPLv2+
++
++Maintainer:
++Jim Meyering
diff --git a/tools/gnulib/patches/647-hashkey-string.patch b/tools/gnulib/patches/647-hashkey-string.patch
new file mode 100644
index 0000000000..d0cd0a6311
--- /dev/null
+++ b/tools/gnulib/patches/647-hashkey-string.patch
@@ -0,0 +1,215 @@
+From 52738dcd0f522b16653cc8b21adfcb758702f2ab Mon Sep 17 00:00:00 2001
+From: Bruno Haible <bruno at clisp.org>
+Date: Wed, 30 Apr 2025 01:20:17 +0200
+Subject: [PATCH] New module hashkey-string.
+
+* lib/hashkey-string.h: New file.
+* lib/hashkey-string.c: New file, based on lib/clean-temp-simple.c.
+* modules/hashkey-string: New file.
+* lib/clean-temp-simple.c: Include hashkey-string.h. Don't include
+<limits.h>.
+(clean_temp_string_equals, clean_temp_string_hash): Remove functions.
+(SIZE_BITS): Remove macro.
+(register_temporary_file): Use hashkey_string_equals and
+hashkey_string_hash.
+* modules/clean-temp-simple (Depends-on): Add hashkey-string.
+---
+ ChangeLog                 | 14 ++++++++++++
+ lib/clean-temp-simple.c   | 33 +++------------------------
+ lib/hashkey-string.c      | 48 +++++++++++++++++++++++++++++++++++++++
+ lib/hashkey-string.h      | 35 ++++++++++++++++++++++++++++
+ modules/clean-temp-simple |  1 +
+ modules/hashkey-string    | 23 +++++++++++++++++++
+ 6 files changed, 124 insertions(+), 30 deletions(-)
+ create mode 100644 lib/hashkey-string.c
+ create mode 100644 lib/hashkey-string.h
+ create mode 100644 modules/hashkey-string
+
+--- a/lib/clean-temp-simple.c
++++ b/lib/clean-temp-simple.c
+@@ -22,7 +22,6 @@
+ #include "clean-temp-private.h"
+ 
+ #include <errno.h>
+-#include <limits.h>
+ #include <signal.h>
+ #include <stdlib.h>
+ #include <string.h>
+@@ -36,6 +35,7 @@
+ #include "thread-optim.h"
+ #include "gl_list.h"
+ #include "gl_linkedhash_list.h"
++#include "hashkey-string.h"
+ #include "gettext.h"
+ 
+ #define _(msgid) dgettext ("gnulib", msgid)
+@@ -106,33 +106,6 @@ gl_list_t /* <closeable_fd *> */ volatil
+         asynchronous signal.
+  */
+ 
+-/* String equality and hash code functions used by the lists.  */
+-
+-bool
+-clean_temp_string_equals (const void *x1, const void *x2)
+-{
+-  const char *s1 = (const char *) x1;
+-  const char *s2 = (const char *) x2;
+-  return strcmp (s1, s2) == 0;
+-}
+-
+-#define SIZE_BITS (sizeof (size_t) * CHAR_BIT)
+-
+-/* A hash function for NUL-terminated char* strings using
+-   the method described by Bruno Haible.
+-   See https://www.haible.de/bruno/hashfunc.html.  */
+-size_t
+-clean_temp_string_hash (const void *x)
+-{
+-  const char *s = (const char *) x;
+-  size_t h = 0;
+-
+-  for (; *s; s++)
+-    h = *s + ((h << 9) | (h >> (SIZE_BITS - 9)));
+-
+-  return h;
+-}
+-
+ 
+ /* The set of fatal signal handlers.
+    Cached here because we are not allowed to call get_fatal_signal_set ()
+@@ -326,8 +299,8 @@ register_temporary_file (const char *abs
+         }
+       file_cleanup_list =
+         gl_list_nx_create_empty (GL_LINKEDHASH_LIST,
+-                                 clean_temp_string_equals,
+-                                 clean_temp_string_hash,
++                                 hashkey_string_equals,
++                                 hashkey_string_hash,
+                                  NULL, false);
+       if (file_cleanup_list == NULL)
+         {
+--- /dev/null
++++ b/lib/hashkey-string.c
+@@ -0,0 +1,48 @@
++/* Support for using a string as a hash key.
++   Copyright (C) 2006-2025 Free Software Foundation, Inc.
++
++   This file is free software: you can redistribute it and/or modify
++   it under the terms of the GNU Lesser General Public License as
++   published by the Free Software Foundation; either version 2.1 of the
++   License, or (at your option) any later version.
++
++   This file is distributed in the hope that it will be useful,
++   but WITHOUT ANY WARRANTY; without even the implied warranty of
++   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
++   GNU Lesser General Public License for more details.
++
++   You should have received a copy of the GNU Lesser General Public License
++   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
++
++#include <config.h>
++
++/* Specification.  */
++#include "hashkey-string.h"
++
++#include <limits.h>
++#include <string.h>
++
++bool
++hashkey_string_equals (const void *x1, const void *x2)
++{
++  const char *s1 = (const char *) x1;
++  const char *s2 = (const char *) x2;
++  return strcmp (s1, s2) == 0;
++}
++
++#define SIZE_BITS (sizeof (size_t) * CHAR_BIT)
++
++/* A hash function for NUL-terminated 'const char *' strings using
++   the method described by Bruno Haible.
++   See https://www.haible.de/bruno/hashfunc.html.  */
++size_t
++hashkey_string_hash (const void *x)
++{
++  const char *s = (const char *) x;
++  size_t h = 0;
++
++  for (; *s; s++)
++    h = *s + ((h << 9) | (h >> (SIZE_BITS - 9)));
++
++  return h;
++}
+--- /dev/null
++++ b/lib/hashkey-string.h
+@@ -0,0 +1,35 @@
++/* Support for using a string as a hash key.
++   Copyright (C) 2006-2025 Free Software Foundation, Inc.
++
++   This file is free software: you can redistribute it and/or modify
++   it under the terms of the GNU Lesser General Public License as
++   published by the Free Software Foundation; either version 2.1 of the
++   License, or (at your option) any later version.
++
++   This file is distributed in the hope that it will be useful,
++   but WITHOUT ANY WARRANTY; without even the implied warranty of
++   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
++   GNU Lesser General Public License for more details.
++
++   You should have received a copy of the GNU Lesser General Public License
++   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
++
++#ifndef _GL_HASHKEY_STRING_H
++#define _GL_HASHKEY_STRING_H
++
++#include <stddef.h>
++
++#ifdef __cplusplus
++extern "C" {
++#endif
++
++/* String equality and hash code functions that operate on plain C strings
++   ('const char *').  */
++extern bool hashkey_string_equals (const void *x1, const void *x2);
++extern size_t hashkey_string_hash (const void *x);
++
++#ifdef __cplusplus
++}
++#endif
++
++#endif /* _GL_HASHKEY_STRING_H */
+--- a/modules/clean-temp-simple
++++ b/modules/clean-temp-simple
+@@ -19,6 +19,7 @@ error
+ fatal-signal
+ rmdir
+ linkedhash-list
++hashkey-string
+ gettext-h
+ gnulib-i18n
+ 
+--- /dev/null
++++ b/modules/hashkey-string
+@@ -0,0 +1,23 @@
++Description:
++Support for using a string as a hash key in the hash-set and hash-map modules.
++
++Files:
++lib/hashkey-string.h
++lib/hashkey-string.c
++
++Depends-on:
++bool
++
++configure.ac:
++
++Makefile.am:
++lib_SOURCES += hashkey-string.h hashkey-string.c
++
++Include:
++"hashkey-string.h"
++
++License:
++LGPLv2+
++
++Maintainer:
++all
diff --git a/tools/gnulib/patches/650-package-version.patch b/tools/gnulib/patches/650-package-version.patch
new file mode 100644
index 0000000000..2e3e046736
--- /dev/null
+++ b/tools/gnulib/patches/650-package-version.patch
@@ -0,0 +1,176 @@
+From e518788ad085e02b046e42889039a1f671e4619a Mon Sep 17 00:00:00 2001
+From: Bruno Haible <bruno at clisp.org>
+Date: Wed, 22 Jan 2025 21:21:59 +0100
+Subject: New module 'package-version'.
+
+* m4/init-package-version.m4: New file, from GNU libunistring.
+* modules/package-version: New file.
+* modules/git-version-gen (Depends-on): Add it.
+---
+ ChangeLog                  |   7 +++
+ m4/init-package-version.m4 | 124 +++++++++++++++++++++++++++++++++++++++++++++
+ modules/git-version-gen    |   1 +
+ modules/package-version    |  19 +++++++
+ 4 files changed, 151 insertions(+)
+ create mode 100644 m4/init-package-version.m4
+ create mode 100644 modules/package-version
+
+--- /dev/null
++++ b/m4/init-package-version.m4
+@@ -0,0 +1,124 @@
++# init-package-version.m4
++# serial 3
++dnl Copyright (C) 1992-2025 Free Software Foundation, Inc.
++dnl This file is free software, distributed under the terms of the GNU
++dnl General Public License.  As a special exception to the GNU General
++dnl Public License, this file may be distributed as part of a program
++dnl that contains a configuration script generated by Autoconf, under
++dnl the same distribution terms as the rest of that program.
++
++# Make it possible to pass version numbers extracted from a file in
++# $(srcdir) to autoconf.
++#
++# Autoconf insists on passing the package name and version number to
++# every generated .h file and every Makefile. This was a reasonable
++# design at times when a version number was changed only once a month.
++# Nowadays, people often assign a new version number once a week, or
++# even change it each time a 'git' commit is made. Regenerating all
++# the files that depend on configure.ac (aclocal.m4, configure,
++# config.status, config.h, all Makefiles) may take 15 minutes. These
++# delays can severely hamper development.
++#
++# An alternative is to store the version number in a file in $(srcdir)
++# that is separate from configure.ac. It can be a data file, a shell
++# script, a .m4 file, or other. The essential point is that the maintainer
++# is responsible for creating Makefile dependencies to this version file
++# for every file that needs to be rebuilt when the version changes. This
++# typically includes
++#   - distributable documentation files that carry the version number,
++# but does not include
++#   - aclocal.m4, configure, config.status, config.h, all Makefiles,
++#   - executables.
++#
++# autoconf and automake make it hard to follow this approach:
++#
++# - If AC_INIT is used with arguments, there is a chicken-and-egg problem:
++#   The arguments need to be read from a file in $(srcdir). The location
++#   of $(srcdir) is only determined by AC_CONFIG_SRCDIR. AC_CONFIG_SRCDIR
++#   can only appear after AC_INIT (otherwise aclocal gives an error:
++#   "error: m4_defn: undefined macro: _m4_divert_diversion").
++#   Furthermore, the arguments passed to AC_INIT must be literals; for
++#   example, the assignment to PACKAGE_VERSION looks like this:
++#     [PACKAGE_VERSION=']AC_PACKAGE_VERSION[']
++#
++# - If AC_INIT is used without arguments:
++#   Automake provides its own variables, PACKAGE and VERSION, and uses them
++#   instead of PACKAGE_NAME and PACKAGE_VERSION that come from Autoconf.
++#   - If AM_INIT_AUTOMAKE is used with two arguments, automake options
++#     like 'silent-rules' cannot be specified.
++#   - If AM_INIT_AUTOMAKE is used in its one-argument form or without
++#     arguments at all, it triggers an error
++#     "error: AC_INIT should be called with package and version arguments".
++#   - If AM_INIT_AUTOMAKE is used in its one-argument form or without
++#     arguments at all, and _AC_INIT_PACKAGE is used before it, with
++#     the package and version number from the file as arguments, we get
++#     a warning: "warning: AC_INIT: not a literal: $VERSION_NUMBER".
++#     The arguments passed to _AC_INIT_PACKAGE must be literals.
++#
++# With the macro defined in this file, the approach can be coded like this:
++#
++#   AC_INIT
++#   AC_CONFIG_SRCDIR(WITNESS)
++#   . $srcdir/../version.sh
++#   gl_INIT_PACKAGE(PACKAGE, $VERSION_NUMBER)
++#   AM_INIT_AUTOMAKE([OPTIONS])
++#
++# and after changing version.sh, the developer can directly configure and build:
++#
++#   make distclean
++#   ./configure
++#   make
++#
++# Some other packages use another approach:
++#
++#   AC_INIT(PACKAGE,
++#           m4_normalize(m4_esyscmd([. ./version.sh; echo $VERSION_NUMBER])))
++#   AC_CONFIG_SRCDIR(WITNESS)
++#   AM_INIT_AUTOMAKE([OPTIONS])
++#
++# but here, after changing version.sh, the developer must first regenerate the
++# configure file:
++#
++#   make distclean
++#   ./autogen.sh --skip-gnulib
++#   ./configure
++#   make
++#
++
++# gl_INIT_PACKAGE(PACKAGE-NAME, VERSION)
++# --------------------------------------
++# followed by an AM_INIT_AUTOMAKE invocation,
++# is like calling AM_INIT_AUTOMAKE(PACKAGE-NAME, VERSION)
++# except that it can use computed non-literal arguments.
++AC_DEFUN([gl_INIT_PACKAGE],
++[
++  AC_BEFORE([$0], [AM_INIT_AUTOMAKE])
++  dnl Redefine AM_INIT_AUTOMAKE.
++  m4_define([gl_AM_INIT_AUTOMAKE],
++    m4_bpatsubst(m4_dquote(
++        m4_bpatsubst(m4_dquote(
++            m4_bpatsubst(m4_dquote(
++                m4_defn([AM_INIT_AUTOMAKE])),
++              [AC_PACKAGE_NAME], [gl_INIT_DUMMY])),
++          [AC_PACKAGE_TARNAME], [gl_INIT_EMPTY])),
++      [AC_PACKAGE_VERSION], [gl_INIT_DUMMY])
++    [AC_SUBST([PACKAGE], [$1])
++     AC_SUBST([VERSION], [$2])
++    ])
++  m4_define([AM_INIT_AUTOMAKE],
++    m4_defn([gl_RPL_INIT_AUTOMAKE]))
++])
++m4_define([gl_INIT_EMPTY], [])
++dnl Automake 1.16.4 no longer accepts an empty value for gl_INIT_DUMMY.
++dnl But a macro that later expands to empty works.
++m4_define([gl_INIT_DUMMY], [gl_INIT_DUMMY2])
++m4_define([gl_INIT_DUMMY2], [])
++AC_DEFUN([gl_RPL_INIT_AUTOMAKE], [
++  m4_ifval([$2],
++    [m4_fatal([After gl_INIT_PACKAGE, the two-argument form of AM_INIT_AUTOMAKE cannot be used.])])
++  gl_AM_INIT_AUTOMAKE([$1 no-define])
++  m4_if(m4_index([ $1 ], [ no-define ]), [-1],
++    [AC_DEFINE_UNQUOTED(PACKAGE, "$PACKAGE", [Name of package])
++     AC_DEFINE_UNQUOTED(VERSION, "$VERSION", [Version number of package])
++    ])
++])
+--- a/modules/git-version-gen
++++ b/modules/git-version-gen
+@@ -5,6 +5,7 @@ Files:
+ build-aux/git-version-gen
+ 
+ Depends-on:
++package-version
+ 
+ configure.ac:
+ 
+--- /dev/null
++++ b/modules/package-version
+@@ -0,0 +1,19 @@
++Description:
++Support for a computed version string.
++
++Files:
++m4/init-package-version.m4
++
++Depends-on:
++
++configure.ac:
++
++Makefile.am:
++
++Include:
++
++License:
++GPLed build tool
++
++Maintainer:
++Bruno Haible
diff --git a/tools/gnulib/patches/651-package-version-simplify.patch b/tools/gnulib/patches/651-package-version-simplify.patch
new file mode 100644
index 0000000000..bed3e65c41
--- /dev/null
+++ b/tools/gnulib/patches/651-package-version-simplify.patch
@@ -0,0 +1,66 @@
+From bb0f82be83d43db9cd77049be32ffd0b92ab5bb7 Mon Sep 17 00:00:00 2001
+From: Bruno Haible <bruno at clisp.org>
+Date: Fri, 24 Jan 2025 22:03:29 +0100
+Subject: package-version: Simplify its use.
+
+Reported by Basil L. Contovounesios <basil at contovou.net> in
+<https://lists.gnu.org/archive/html/bug-gnulib/2025-01/msg00195.html>.
+
+* doc/package-version.texi (Propagating the package version): Recommend
+to pass the usual arguments to AC_INIT.
+* m4/init-package-version.m4: Likewise.
+(gl_INIT_PACKAGE): Define PACKAGE_VERSION and PACKAGE_STRING as needed.
+(gl_RPL_INIT_AUTOMAKE): Improve quoting.
+---
+ ChangeLog                  | 11 +++++++++++
+ doc/package-version.texi   |  2 +-
+ m4/init-package-version.m4 | 20 ++++++++++++++------
+ 3 files changed, 26 insertions(+), 7 deletions(-)
+
+--- a/m4/init-package-version.m4
++++ b/m4/init-package-version.m4
+@@ -1,5 +1,5 @@
+ # init-package-version.m4
+-# serial 3
++# serial 4
+ dnl Copyright (C) 1992-2025 Free Software Foundation, Inc.
+ dnl This file is free software, distributed under the terms of the GNU
+ dnl General Public License.  As a special exception to the GNU General
+@@ -57,7 +57,7 @@ dnl the same distribution terms as the r
+ #
+ # With the macro defined in this file, the approach can be coded like this:
+ #
+-#   AC_INIT
++#   AC_INIT(PACKAGE, [dummy], [MORE OPTIONS])
+ #   AC_CONFIG_SRCDIR(WITNESS)
+ #   . $srcdir/../version.sh
+ #   gl_INIT_PACKAGE(PACKAGE, $VERSION_NUMBER)
+@@ -102,8 +102,16 @@ AC_DEFUN([gl_INIT_PACKAGE],
+               [AC_PACKAGE_NAME], [gl_INIT_DUMMY])),
+           [AC_PACKAGE_TARNAME], [gl_INIT_EMPTY])),
+       [AC_PACKAGE_VERSION], [gl_INIT_DUMMY])
+-    [AC_SUBST([PACKAGE], [$1])
+-     AC_SUBST([VERSION], [$2])
++    [dnl Set variables documented in Automake.
++     AC_SUBST([PACKAGE], [$1])
++     AC_SUBST([VERSION], ["$2"])
++     dnl Set variables documented in Autoconf.
++     AC_SUBST([PACKAGE_VERSION], ["$2"])
++     AC_SUBST([PACKAGE_STRING], ["$1 $2"])
++     AC_DEFINE_UNQUOTED([PACKAGE_VERSION], ["$2"],
++       [Define to the version of this package.])
++     AC_DEFINE_UNQUOTED([PACKAGE_STRING], ["$1 $2"],
++       [Define to the full name and version of this package.])
+     ])
+   m4_define([AM_INIT_AUTOMAKE],
+     m4_defn([gl_RPL_INIT_AUTOMAKE]))
+@@ -118,7 +126,7 @@ AC_DEFUN([gl_RPL_INIT_AUTOMAKE], [
+     [m4_fatal([After gl_INIT_PACKAGE, the two-argument form of AM_INIT_AUTOMAKE cannot be used.])])
+   gl_AM_INIT_AUTOMAKE([$1 no-define])
+   m4_if(m4_index([ $1 ], [ no-define ]), [-1],
+-    [AC_DEFINE_UNQUOTED(PACKAGE, "$PACKAGE", [Name of package])
+-     AC_DEFINE_UNQUOTED(VERSION, "$VERSION", [Version number of package])
++    [AC_DEFINE_UNQUOTED([PACKAGE], ["$PACKAGE"], [Name of package])
++     AC_DEFINE_UNQUOTED([VERSION], ["$VERSION"], [Version number of package])
+     ])
+ ])
diff --git a/tools/gnulib/patches/652-package-version-simplify-further.patch b/tools/gnulib/patches/652-package-version-simplify-further.patch
new file mode 100644
index 0000000000..5458918246
--- /dev/null
+++ b/tools/gnulib/patches/652-package-version-simplify-further.patch
@@ -0,0 +1,89 @@
+From 48648b4b9b3fd79a5c68913deb28678bd9d8eb34 Mon Sep 17 00:00:00 2001
+From: Bruno Haible <bruno at clisp.org>
+Date: Sat, 25 Jan 2025 04:07:32 +0100
+Subject: package-version: Simplify further.
+
+* doc/package-version.texi (Propagating the package version): Recommend
+use of gl_INIT_PACKAGE_VERSION instead of gl_INIT_PACKAGE.
+* build-aux/git-version-gen: Likewise.
+* m4/init-package-version.m4: Likewise.
+(gl_INIT_PACKAGE_VERSION): Renamed from gl_INIT_PACKAGE. Take only one
+argument. Don't fiddle with AC_PACKAGE_NAME, AC_PACKAGE_TARNAME,
+PACKAGE.
+(gl_RPL_INIT_AUTOMAKE): Update.
+---
+ ChangeLog                  | 10 ++++++++++
+ build-aux/git-version-gen  |  4 ++--
+ doc/package-version.texi   |  4 ++--
+ m4/init-package-version.m4 | 30 ++++++++++++------------------
+ 4 files changed, 26 insertions(+), 22 deletions(-)
+
+--- a/m4/init-package-version.m4
++++ b/m4/init-package-version.m4
+@@ -1,5 +1,5 @@
+ # init-package-version.m4
+-# serial 4
++# serial 5
+ dnl Copyright (C) 1992-2025 Free Software Foundation, Inc.
+ dnl This file is free software, distributed under the terms of the GNU
+ dnl General Public License.  As a special exception to the GNU General
+@@ -60,7 +60,7 @@ dnl the same distribution terms as the r
+ #   AC_INIT(PACKAGE, [dummy], [MORE OPTIONS])
+ #   AC_CONFIG_SRCDIR(WITNESS)
+ #   . $srcdir/../version.sh
+-#   gl_INIT_PACKAGE(PACKAGE, $VERSION_NUMBER)
++#   gl_INIT_PACKAGE_VERSION($VERSION_NUMBER)
+ #   AM_INIT_AUTOMAKE([OPTIONS])
+ #
+ # and after changing version.sh, the developer can directly configure and build:
+@@ -85,32 +85,26 @@ dnl the same distribution terms as the r
+ #   make
+ #
+ 
+-# gl_INIT_PACKAGE(PACKAGE-NAME, VERSION)
+-# --------------------------------------
++# gl_INIT_PACKAGE_VERSION(VERSION)
++# --------------------------------
+ # followed by an AM_INIT_AUTOMAKE invocation,
+ # is like calling AM_INIT_AUTOMAKE(PACKAGE-NAME, VERSION)
+ # except that it can use computed non-literal arguments.
+-AC_DEFUN([gl_INIT_PACKAGE],
++AC_DEFUN([gl_INIT_PACKAGE_VERSION],
+ [
+   AC_BEFORE([$0], [AM_INIT_AUTOMAKE])
+   dnl Redefine AM_INIT_AUTOMAKE.
+   m4_define([gl_AM_INIT_AUTOMAKE],
+-    m4_bpatsubst(m4_dquote(
+-        m4_bpatsubst(m4_dquote(
+-            m4_bpatsubst(m4_dquote(
+-                m4_defn([AM_INIT_AUTOMAKE])),
+-              [AC_PACKAGE_NAME], [gl_INIT_DUMMY])),
+-          [AC_PACKAGE_TARNAME], [gl_INIT_EMPTY])),
++    m4_bpatsubst(m4_dquote(m4_defn([AM_INIT_AUTOMAKE])),
+       [AC_PACKAGE_VERSION], [gl_INIT_DUMMY])
+     [dnl Set variables documented in Automake.
+-     AC_SUBST([PACKAGE], [$1])
+-     AC_SUBST([VERSION], ["$2"])
++     AC_SUBST([VERSION], ["$1"])
+      dnl Set variables documented in Autoconf.
+-     AC_SUBST([PACKAGE_VERSION], ["$2"])
+-     AC_SUBST([PACKAGE_STRING], ["$1 $2"])
+-     AC_DEFINE_UNQUOTED([PACKAGE_VERSION], ["$2"],
++     AC_SUBST([PACKAGE_VERSION], ["$1"])
++     AC_SUBST([PACKAGE_STRING], ["AC_PACKAGE_NAME $1"])
++     AC_DEFINE_UNQUOTED([PACKAGE_VERSION], ["$1"],
+        [Define to the version of this package.])
+-     AC_DEFINE_UNQUOTED([PACKAGE_STRING], ["$1 $2"],
++     AC_DEFINE_UNQUOTED([PACKAGE_STRING], ["AC_PACKAGE_NAME $1"],
+        [Define to the full name and version of this package.])
+     ])
+   m4_define([AM_INIT_AUTOMAKE],
+@@ -123,7 +117,7 @@ m4_define([gl_INIT_DUMMY], [gl_INIT_DUMM
+ m4_define([gl_INIT_DUMMY2], [])
+ AC_DEFUN([gl_RPL_INIT_AUTOMAKE], [
+   m4_ifval([$2],
+-    [m4_fatal([After gl_INIT_PACKAGE, the two-argument form of AM_INIT_AUTOMAKE cannot be used.])])
++    [m4_fatal([After gl_INIT_PACKAGE_VERSION, the two-argument form of AM_INIT_AUTOMAKE cannot be used.])])
+   gl_AM_INIT_AUTOMAKE([$1 no-define])
+   m4_if(m4_index([ $1 ], [ no-define ]), [-1],
+     [AC_DEFINE_UNQUOTED([PACKAGE], ["$PACKAGE"], [Name of package])
diff --git a/tools/gnulib/patches/653-package-version-warning.patch b/tools/gnulib/patches/653-package-version-warning.patch
new file mode 100644
index 0000000000..4baa21ce65
--- /dev/null
+++ b/tools/gnulib/patches/653-package-version-warning.patch
@@ -0,0 +1,32 @@
+From 2e46209809f751087ca27523283bd5c3e9071d31 Mon Sep 17 00:00:00 2001
+From: Bruno Haible <bruno at clisp.org>
+Date: Sun, 26 Jan 2025 13:26:35 +0100
+Subject: package-version: Avoid compiler warnings in config.log.
+
+* m4/init-package-version.m4 (gl_INIT_PACKAGE_VERSION): Undefine
+PACKAGE_VERSION and PACKAGE_STRING before redefining them.
+---
+ ChangeLog                  | 6 ++++++
+ m4/init-package-version.m4 | 4 +++-
+ 2 files changed, 9 insertions(+), 1 deletion(-)
+
+--- a/m4/init-package-version.m4
++++ b/m4/init-package-version.m4
+@@ -1,5 +1,5 @@
+ # init-package-version.m4
+-# serial 5
++# serial 6
+ dnl Copyright (C) 1992-2025 Free Software Foundation, Inc.
+ dnl This file is free software, distributed under the terms of the GNU
+ dnl General Public License.  As a special exception to the GNU General
+@@ -102,8 +102,10 @@ AC_DEFUN([gl_INIT_PACKAGE_VERSION],
+      dnl Set variables documented in Autoconf.
+      AC_SUBST([PACKAGE_VERSION], ["$1"])
+      AC_SUBST([PACKAGE_STRING], ["AC_PACKAGE_NAME $1"])
++     _AC_DEFINE([#undef PACKAGE_VERSION])
+      AC_DEFINE_UNQUOTED([PACKAGE_VERSION], ["$1"],
+        [Define to the version of this package.])
++     _AC_DEFINE([#undef PACKAGE_STRING])
+      AC_DEFINE_UNQUOTED([PACKAGE_STRING], ["AC_PACKAGE_NAME $1"],
+        [Define to the full name and version of this package.])
+     ])
diff --git a/tools/gnulib/patches/660-version-stamp.patch b/tools/gnulib/patches/660-version-stamp.patch
new file mode 100644
index 0000000000..d85c0455cf
--- /dev/null
+++ b/tools/gnulib/patches/660-version-stamp.patch
@@ -0,0 +1,75 @@
+From 85599643e2fbf70f7f0bd58831993132ef335705 Mon Sep 17 00:00:00 2001
+From: Bruno Haible <bruno at clisp.org>
+Date: Wed, 22 Jan 2025 21:25:27 +0100
+Subject: New module 'version-stamp'.
+
+* m4/version-stamp.m4: New file.
+* modules/version-stamp: New file.
+---
+ ChangeLog             |  6 ++++++
+ m4/version-stamp.m4   | 35 +++++++++++++++++++++++++++++++++++
+ modules/version-stamp | 19 +++++++++++++++++++
+ 3 files changed, 60 insertions(+)
+ create mode 100644 m4/version-stamp.m4
+ create mode 100644 modules/version-stamp
+
+--- /dev/null
++++ b/m4/version-stamp.m4
+@@ -0,0 +1,35 @@
++# version-stamp.m4
++# serial 1
++dnl Copyright (C) 2025 Free Software Foundation, Inc.
++dnl This file is free software, distributed under the terms of the GNU
++dnl General Public License.  As a special exception to the GNU General
++dnl Public License, this file may be distributed as part of a program
++dnl that contains a configuration script generated by Autoconf, under
++dnl the same distribution terms as the rest of that program.
++
++# Manages a stamp file, that keeps track when $(VERSION) was last changed.
++#
++# gl_CONFIG_VERSION_STAMP
++# needs to be invoked near the end of the package's top-level configure.ac,
++# before AC_OUTPUT.
++# It makes sure that during the build,
++#   - $(top_srcdir)/.version exists, and
++#   - when $(VERSION) is changed, $(top_srcdir)/.version gets modified.
++#
++# $(top_srcdir)/.version is a stamp file. Its contents wouldn't matter,
++# except that for detecting the change, we store the value of $(VERSION)
++# in it (but we could just as well store it in a different file).
++AC_DEFUN([gl_CONFIG_VERSION_STAMP],
++[
++  AC_CONFIG_COMMANDS([version-timestamp],
++    [if test -f "$ac_top_srcdir/.version" \
++        && test `cat "$ac_top_srcdir/.version"` = "$gl_version"; then
++       # The value of $(VERSION) is the same as last time.
++       :
++     else
++       # The value of $(VERSION) has changed. Update the stamp.
++       echo "$gl_version" > "$ac_top_srcdir/.version"
++     fi
++    ],
++    [gl_version="$VERSION"])
++])
+--- /dev/null
++++ b/modules/version-stamp
+@@ -0,0 +1,19 @@
++Description:
++Optimized rebuilding of artifacts that depend on $(VERSION).
++
++Files:
++m4/version-stamp.m4
++
++Depends-on:
++
++configure.ac:
++
++Makefile.am:
++
++Include:
++
++License:
++GPLed build tool
++
++Maintainer:
++Bruno Haible
diff --git a/tools/gnulib/patches/689-vc-mtime.patch b/tools/gnulib/patches/689-vc-mtime.patch
new file mode 100644
index 0000000000..62638d7ff4
--- /dev/null
+++ b/tools/gnulib/patches/689-vc-mtime.patch
@@ -0,0 +1,366 @@
+From 701d20aaf579bb71f35209dd63a272c3d9d21096 Mon Sep 17 00:00:00 2001
+From: Bruno Haible <bruno at clisp.org>
+Date: Mon, 24 Feb 2025 19:03:17 +0100
+Subject: [PATCH] vc-mtime: New module.
+
+* lib/vc-mtime.h: New file.
+* lib/vc-mtime.c: New file.
+* modules/vc-mtime: New file.
+---
+ ChangeLog        |   7 ++
+ lib/vc-mtime.c   | 208 +++++++++++++++++++++++++++++++++++++++++++++++
+ lib/vc-mtime.h   |  97 ++++++++++++++++++++++
+ modules/vc-mtime |  34 ++++++++
+ 4 files changed, 346 insertions(+)
+ create mode 100644 lib/vc-mtime.c
+ create mode 100644 lib/vc-mtime.h
+ create mode 100644 modules/vc-mtime
+
+--- /dev/null
++++ b/lib/vc-mtime.c
+@@ -0,0 +1,208 @@
++/* Return the version-control based modification time of a file.
++   Copyright (C) 2025 Free Software Foundation, Inc.
++
++   This program is free software: you can redistribute it and/or modify
++   it under the terms of the GNU General Public License as published by
++   the Free Software Foundation, either version 3 of the License, or
++   (at your option) any later version.
++
++   This program is distributed in the hope that it will be useful,
++   but WITHOUT ANY WARRANTY; without even the implied warranty of
++   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
++   GNU General Public License for more details.
++
++   You should have received a copy of the GNU General Public License
++   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
++
++/* Written by Bruno Haible <bruno at clisp.org>, 2025.  */
++
++#include <config.h>
++
++/* Specification.  */
++#include "vc-mtime.h"
++
++#include <stdlib.h>
++#include <unistd.h>
++
++#include <error.h>
++#include "spawn-pipe.h"
++#include "wait-process.h"
++#include "execute.h"
++#include "safe-read.h"
++#include "xstrtol.h"
++#include "stat-time.h"
++#include "gettext.h"
++
++#define _(msgid) dgettext ("gnulib", msgid)
++
++
++/* Determines whether the specified file is under version control.  */
++static bool
++git_vc_controlled (const char *filename)
++{
++  /* Run "git ls-files FILENAME" and return true if the exit code is 0
++     and the output is non-empty.  */
++  const char *argv[4];
++  pid_t child;
++  int fd[1];
++
++  argv[0] = "git";
++  argv[1] = "ls-files";
++  argv[2] = filename;
++  argv[3] = NULL;
++  child = create_pipe_in ("git", "git", argv, NULL, NULL,
++                          DEV_NULL, true, true, false, fd);
++  if (child == -1)
++    return false;
++
++  /* Read the subprocess output, and test whether it is non-empty.  */
++  size_t count = 0;
++  char c;
++
++  while (safe_read (fd[0], &c, 1) > 0)
++    count++;
++
++  close (fd[0]);
++
++  /* Remove zombie process from process list, and retrieve exit status.  */
++  int exitstatus =
++    wait_subprocess (child, "git", false, true, true, false, NULL);
++  return (exitstatus == 0 && count > 0);
++}
++
++/* Determines whether the specified file is unmodified, compared to the
++   last version in version control.  */
++static bool
++git_unmodified (const char *filename)
++{
++  /* Run "git diff --quiet -- HEAD FILENAME"
++     (or "git diff --quiet HEAD FILENAME")
++     and return true if the exit code is 0.
++     The '--' option is for the case that the specified file was removed.  */
++  const char *argv[7];
++  int exitstatus;
++
++  argv[0] = "git";
++  argv[1] = "diff";
++  argv[2] = "--quiet";
++  argv[3] = "--";
++  argv[4] = "HEAD";
++  argv[5] = filename;
++  argv[6] = NULL;
++  exitstatus = execute ("git", "git", argv, NULL, NULL,
++                        false, false, true, true,
++                        true, false, NULL);
++  return (exitstatus == 0);
++}
++
++/* Stores in *MTIME the time of last modification in version control of the
++   specified file, and returns 0.
++   Upon failure, it returns -1.  */
++static int
++git_mtime (struct timespec *mtime, const char *filename)
++{
++  /* Run "git log -1 --format=%ct -- FILENAME".  It prints the time of last
++     modification, as the number of seconds since the Epoch.
++     The '--' option is for the case that the specified file was removed.  */
++  const char *argv[7];
++  pid_t child;
++  int fd[1];
++
++  argv[0] = "git";
++  argv[1] = "log";
++  argv[2] = "-1";
++  argv[3] = "--format=%ct";
++  argv[4] = "--";
++  argv[5] = filename;
++  argv[6] = NULL;
++  child = create_pipe_in ("git", "git", argv, NULL, NULL,
++                          DEV_NULL, true, true, false, fd);
++  if (child == -1)
++    return -1;
++
++  /* Retrieve its result.  */
++  FILE *fp;
++  char *line;
++  size_t linesize;
++  size_t linelen;
++
++  fp = fdopen (fd[0], "r");
++  if (fp == NULL)
++    error (EXIT_FAILURE, errno, _("fdopen() failed"));
++
++  line = NULL; linesize = 0;
++  linelen = getline (&line, &linesize, fp);
++  if (linelen == (size_t)(-1))
++    {
++      error (0, 0, _("%s subprocess I/O error"), "git");
++      fclose (fp);
++      wait_subprocess (child, "git", true, false, true, false, NULL);
++    }
++  else
++    {
++      int exitstatus;
++
++      if (linelen > 0 && line[linelen - 1] == '\n')
++        line[linelen - 1] = '\0';
++
++      fclose (fp);
++
++      /* Remove zombie process from process list, and retrieve exit status.  */
++      exitstatus =
++        wait_subprocess (child, "git", true, false, true, false, NULL);
++      if (exitstatus == 0)
++        {
++          char *endptr;
++          unsigned long git_log_time;
++          if (xstrtoul (line, &endptr, 10, &git_log_time, NULL) == LONGINT_OK
++              && endptr == line + strlen (line))
++            {
++              mtime->tv_sec = git_log_time;
++              mtime->tv_nsec = 0;
++              free (line);
++              return 0;
++            }
++        }
++    }
++  free (line);
++  return -1;
++}
++
++int
++vc_mtime (struct timespec *mtime, const char *filename)
++{
++  static bool git_tested;
++  static bool git_present;
++
++  if (!git_tested)
++    {
++      /* Test for presence of git:
++         "git --version >/dev/null 2>/dev/null"  */
++      const char *argv[3];
++      int exitstatus;
++
++      argv[0] = "git";
++      argv[1] = "--version";
++      argv[2] = NULL;
++      exitstatus = execute ("git", "git", argv, NULL, NULL,
++                            false, false, true, true,
++                            true, false, NULL);
++      git_present = (exitstatus == 0);
++      git_tested = true;
++    }
++
++  if (git_present
++      && git_vc_controlled (filename)
++      && git_unmodified (filename))
++    {
++      if (git_mtime (mtime, filename) == 0)
++        return 0;
++    }
++  struct stat statbuf;
++  if (stat (filename, &statbuf) == 0)
++    {
++      *mtime = get_stat_mtime (&statbuf);
++      return 0;
++    }
++  return -1;
++}
+--- /dev/null
++++ b/lib/vc-mtime.h
+@@ -0,0 +1,97 @@
++/* Return the version-control based modification time of a file.
++   Copyright (C) 2025 Free Software Foundation, Inc.
++
++   This program is free software: you can redistribute it and/or modify
++   it under the terms of the GNU General Public License as published by
++   the Free Software Foundation, either version 3 of the License, or
++   (at your option) any later version.
++
++   This program is distributed in the hope that it will be useful,
++   but WITHOUT ANY WARRANTY; without even the implied warranty of
++   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
++   GNU General Public License for more details.
++
++   You should have received a copy of the GNU General Public License
++   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
++
++/* Written by Bruno Haible <bruno at clisp.org>, 2025.  */
++
++#ifndef _VC_MTIME_H
++#define _VC_MTIME_H
++
++/* Get struct timespec.  */
++#include <time.h>
++
++/* The "version-controlled modification time" vc_mtime(F) of a file F
++   is defined as:
++     - If F is under version control and not modified locally:
++       the time of the last change of F in the version control system.
++     - Otherwise: The modification time of F on disk.
++
++   For now, the only VCS supported by this module is git.  (hg and svn are
++   hardly in use any more.)
++
++   This has the properties that:
++     - Different users who have checked out the same git repo on different
++       machines, at different times, and not done local modifications,
++       get the same vc_mtime(F).
++     - If a user has modified F locally, the modification time of that file
++       counts.
++     - If that user then reverts the modification, they then again get the
++       same vc_mtime(F) as everyone else.
++     - Different users who have unpacked the same tarball (without .git
++       directory) on different machines, at different times, also get the same
++       vc_mtime(F) [but possibly a different one than when the .git directory
++       was present].  (Assuming a POSIX compliant file system.)
++     - When a user commits local modifications into git, this only increases
++       (not decreases) the vc_mtime(F).
++
++   The purpose of the version-controlled modification time is to produce a
++   reproducible timestamp(Z) of a file Z that depends on files X1, ..., Xn,
++   in such a way that
++     - timestamp(Z) is reproducible, that is, different users on different
++       machines get the same value.
++     - timestamp(Z) is related to reality.  It's not just a dummy, like what
++       is suggested in <https://reproducible-builds.org/docs/timestamps/>.
++     - One can arrange for timestamp(Z) to respect the modification time
++       relations of a build system.
++
++   There are two uses of such a timestamp:
++     - It can be set as the modification time of file Z in a file system, or
++     - It can be embedded in Z, with the purpose of telling a user how old
++       the file Z is.  For example, in PDF files or in generated documentation,
++       such a time is embedded in a special place.
++
++   The simplest example is a file Z that depends on files X1, ..., Xn.
++   Generally one will define
++     timestamp(Z) = max (vc_mtime(X1), ..., vc_mtime(Xn))
++   for an embedded timestamp, or
++     timestamp(Z) = max (vc_mtime(X1), ..., vc_mtime(Xn)) + 1 second
++   for a time stamp in a file system.  The added second
++     1. accounts for fractional seconds in mtime(X1), ..., mtime(Xn),
++     2. allows for 'make' implementation that attempt to rebuild Z
++        if mtime(Z) == mtime(Xi).
++
++   A more complicated example is when there are intermediate built files, not
++   under version control. For example, if the build process produces
++     X1, X2 -> Y1
++     X3, X4 -> Y2
++     Y1, Y2, X5 -> Z
++   where Y1 and Y2 are intermediate built files, you should ignore the
++   mtime(Y1), mtime(Y2), and consider only the vc_mtime(X1), ..., vc_mtime(X5).
++ */
++
++#ifdef __cplusplus
++extern "C" {
++#endif
++
++/* Determines the version-controlled modification time of FILENAME, stores it
++   in *MTIME, and returns 0.
++   Upon failure, it returns -1.  */
++extern int vc_mtime (struct timespec *mtime, const char *filename);
++
++#ifdef __cplusplus
++}
++#endif
++
++#endif /* _VC_MTIME_H */
+--- /dev/null
++++ b/modules/vc-mtime
+@@ -0,0 +1,34 @@
++Description:
++Returns the version-control based modification time of a file.
++
++Files:
++lib/vc-mtime.h
++lib/vc-mtime.c
++
++Depends-on:
++time-h
++bool
++spawn-pipe
++wait-process
++execute
++safe-read
++error
++getline
++xstrtol
++stat-time
++gettext-h
++gnulib-i18n
++
++configure.ac:
++
++Makefile.am:
++lib_SOURCES += vc-mtime.c
++
++Include:
++"vm-mtime.h"
++
++License:
++GPL
++
++Maintainer:
++Bruno Haible
diff --git a/tools/gnulib/patches/755-clean-temp-hashkey.patch b/tools/gnulib/patches/755-clean-temp-hashkey.patch
new file mode 100644
index 0000000000..7a4774c9b3
--- /dev/null
+++ b/tools/gnulib/patches/755-clean-temp-hashkey.patch
@@ -0,0 +1,64 @@
+From f47c5f2e21d0ccedb271b406e35b6963b23a64c4 Mon Sep 17 00:00:00 2001
+From: Bruno Haible <bruno at clisp.org>
+Date: Wed, 30 Apr 2025 13:11:01 +0200
+Subject: [PATCH] clean-temp: Fix link error (regression yesterday).
+
+* lib/clean-temp.c: Include hashkey-string.h.
+(create_temp_dir): Use hashkey_string_* functions instead of
+clean_temp_string_*.
+* lib/clean-temp-private.h (clean_temp_string_equals,
+clean_temp_string_hash): Remove declarations.
+* modules/clean-temp (Depends-on): Add hashkey-string.
+---
+ ChangeLog                | 10 ++++++++++
+ lib/clean-temp-private.h |  3 ---
+ lib/clean-temp.c         |  5 +++--
+ modules/clean-temp       |  1 +
+ 4 files changed, 14 insertions(+), 5 deletions(-)
+
+--- a/lib/clean-temp-private.h
++++ b/lib/clean-temp-private.h
+@@ -68,9 +68,6 @@ struct closeable_fd
+ #define descriptors clean_temp_descriptors
+ extern gl_list_t /* <closeable_fd *> */ volatile descriptors;
+ 
+-extern bool clean_temp_string_equals (const void *x1, const void *x2);
+-extern size_t clean_temp_string_hash (const void *x);
+-
+ extern _GL_ASYNC_SAFE int clean_temp_asyncsafe_close (struct closeable_fd *element);
+ extern void clean_temp_init_asyncsafe_close (void);
+ 
+--- a/lib/clean-temp.c
++++ b/lib/clean-temp.c
+@@ -45,6 +45,7 @@
+ #include "xmalloca.h"
+ #include "glthread/lock.h"
+ #include "thread-optim.h"
++#include "hashkey-string.h"
+ #include "gl_xlist.h"
+ #include "gl_linkedhash_list.h"
+ #include "gl_linked_list.h"
+@@ -221,11 +222,11 @@ create_temp_dir (const char *prefix, con
+   tmpdir->cleanup_verbose = cleanup_verbose;
+   tmpdir->subdirs =
+     gl_list_create_empty (GL_LINKEDHASH_LIST,
+-                          clean_temp_string_equals, clean_temp_string_hash,
++                          hashkey_string_equals, hashkey_string_hash,
+                           NULL, false);
+   tmpdir->files =
+     gl_list_create_empty (GL_LINKEDHASH_LIST,
+-                          clean_temp_string_equals, clean_temp_string_hash,
++                          hashkey_string_equals, hashkey_string_hash,
+                           NULL, false);
+ 
+   /* Create the temporary directory.  */
+--- a/modules/clean-temp
++++ b/modules/clean-temp
+@@ -24,6 +24,7 @@ rmdir
+ xalloc
+ xalloc-die
+ xmalloca
++hashkey-string
+ linkedhash-list
+ linked-list
+ xlist
diff --git a/tools/gnulib/patches/795-string-desc-rename-functions.patch b/tools/gnulib/patches/795-string-desc-rename-functions.patch
new file mode 100644
index 0000000000..032545b063
--- /dev/null
+++ b/tools/gnulib/patches/795-string-desc-rename-functions.patch
@@ -0,0 +1,1137 @@
+From 9a26e7043fa95b4c9ee4576ce8c0ac15668e695e Mon Sep 17 00:00:00 2001
+From: Bruno Haible <bruno at clisp.org>
+Date: Thu, 2 Jan 2025 13:54:54 +0100
+Subject: [PATCH] string-desc, xstring-desc, string-desc-quotearg: Rename
+ functions.
+
+* lib/string-desc.h (sd_equals): Renamed from string_desc_equals.
+(sd_startswith): Renamed from string_desc_startswith.
+(sd_endswith): Renamed from string_desc_endswith.
+(sd_cmp): Renamed from string_desc_cmp.
+(sd_c_casecmp): Renamed from string_desc_c_casecmp.
+(sd_index): Renamed from string_desc_index.
+(sd_last_index): Renamed from string_desc_last_index.
+(sd_contains): Renamed from string_desc_contains.
+(sd_new_empty): Renamed from string_desc_new_empty.
+(sd_new_addr): Renamed from string_desc_new_addr.
+(sd_from_c): Renamed from string_desc_from_c.
+(sd_substring): Renamed from string_desc_substring.
+(sd_write): Renamed from string_desc_write.
+(sd_fwrite): Renamed from string_desc_fwrite.
+(sd_new): Renamed from string_desc_new.
+(sd_new_filled): Renamed from string_desc_new_filled.
+(sd_copy): Renamed from string_desc_copy.
+(sd_concat): Renamed from string_desc_concat.
+(sd_c): Renamed from string_desc_c.
+(sd_set_char_at): Renamed from string_desc_set_char_at.
+(sd_fill): Renamed from string_desc_fill.
+(sd_overwrite): Renamed from string_desc_overwrite.
+(sd_free): Renamed from string_desc_free.
+(sd_length): Renamed from string_desc_length.
+(sd_char_at): Renamed from string_desc_char_at.
+(sd_data): Renamed from string_desc_data.
+(sd_is_empty): Renamed from string_desc_is_empty.
+* lib/string-desc.c (sd_equals): Renamed from string_desc_equals.
+(sd_startswith): Renamed from string_desc_startswith.
+(sd_endswith): Renamed from string_desc_endswith.
+(sd_cmp): Renamed from string_desc_cmp.
+(sd_c_casecmp): Renamed from string_desc_c_casecmp.
+(sd_index): Renamed from string_desc_index.
+(sd_last_index): Renamed from string_desc_last_index.
+(sd_new_empty): Renamed from string_desc_new_empty.
+(sd_new_addr): Renamed from string_desc_new_addr.
+(sd_from_c): Renamed from string_desc_from_c.
+(sd_substring): Renamed from string_desc_substring.
+(sd_write): Renamed from string_desc_write.
+(sd_fwrite): Renamed from string_desc_fwrite.
+(sd_new): Renamed from string_desc_new.
+(sd_new_filled): Renamed from string_desc_new_filled.
+(sd_copy): Renamed from string_desc_copy.
+(sd_concat): Renamed from string_desc_concat.
+(sd_c): Renamed from string_desc_c.
+(sd_set_char_at): Renamed from string_desc_set_char_at.
+(sd_fill): Renamed from string_desc_fill.
+(sd_overwrite): Renamed from string_desc_overwrite.
+(sd_free): Renamed from string_desc_free.
+* lib/xstring-desc.h (xsd_concat): Renamed from xstring_desc_concat.
+(xsd_new): Renamed from xstring_desc_new.
+(xsd_new_filled): Renamed from xstring_desc_new_filled.
+(xsd_copy): Renamed from xstring_desc_copy.
+(xsd_c): Renamed from xstring_desc_c.
+* lib/xstring-desc.c (xsd_concat): Renamed from xstring_desc_concat.
+* lib/string-desc-quotearg.h (sd_quotearg_buffer): Renamed from
+string_desc_quotearg_buffer.
+(sd_quotearg_alloc): Renamed from string_desc_quotearg_alloc.
+(sd_quotearg_n): Renamed from string_desc_quotearg_n.
+(sd_quotearg): Renamed from string_desc_quotearg.
+(sd_quotearg_n_style): Renamed from string_desc_quotearg_n_style.
+(sd_quotearg_style): Renamed from string_desc_quotearg_style.
+(sd_quotearg_char): Renamed from string_desc_quotearg_char.
+(sd_quotearg_colon): Renamed from string_desc_quotearg_colon.
+(sd_quotearg_n_custom): Renamed from string_desc_quotearg_n_custom.
+(sd_quotearg_custom): Renamed from sd_quotearg_n_custom.
+* lib/string-desc-contains.c (sd_contains): Renamed from
+string_desc_contains.
+* lib/string-buffer.h: Update.
+* lib/string-buffer.c (sb_append_desc, sb_contents, sb_dupfree): Update.
+* lib/xstring-buffer.c (sb_xdupfree): Update.
+* lib/sf-istream.c (sf_istream_init_from_string_desc): Update.
+* tests/test-string-desc.c (main): Update.
+* tests/test-string-desc.sh: Update.
+* tests/test-xstring-desc.c (main): Update.
+* tests/test-string-desc-quotearg.c (main): Update.
+* tests/test-string-buffer.c (main): Update.
+* tests/test-sf-istream.c (main): Update.
+* tests/test-sfl-istream.c (main): Update.
+* doc/string-desc.texi: Update.
+* doc/strings.texi: Update.
+* NEWS: Mention the change.
+---
+ ChangeLog                         |  86 +++++++++
+ NEWS                              |   4 +
+ doc/string-desc.texi              |   4 +-
+ doc/strings.texi                  |  18 +-
+ lib/sf-istream.c                  |   4 +-
+ lib/string-buffer.c               |  12 +-
+ lib/string-buffer.h               |   4 +-
+ lib/string-desc-contains.c        |   2 +-
+ lib/string-desc-quotearg.h        | 114 ++++++------
+ lib/string-desc.c                 |  52 +++---
+ lib/string-desc.h                 |  62 +++----
+ lib/xstring-buffer.c              |   4 +-
+ lib/xstring-desc.c                |   2 +-
+ lib/xstring-desc.h                |  26 +--
+ tests/test-sf-istream.c           |   4 +-
+ tests/test-sfl-istream.c          |   4 +-
+ tests/test-string-buffer.c        |  18 +-
+ tests/test-string-desc-quotearg.c |  44 ++---
+ tests/test-string-desc.c          | 296 +++++++++++++++---------------
+ tests/test-string-desc.sh         |   4 +-
+ tests/test-xstring-desc.c         |  68 +++----
+ 21 files changed, 461 insertions(+), 371 deletions(-)
+
+--- a/lib/sf-istream.c
++++ b/lib/sf-istream.c
+@@ -46,8 +46,8 @@ sf_istream_init_from_string_desc (sf_ist
+                                   string_desc_t input)
+ {
+   stream->fp = NULL;
+-  stream->input = string_desc_data (input);
+-  stream->input_end = stream->input + string_desc_length (input);
++  stream->input = sd_data (input);
++  stream->input_end = stream->input + sd_length (input);
+ }
+ 
+ int
+--- a/lib/string-buffer.c
++++ b/lib/string-buffer.c
+@@ -101,13 +101,13 @@ sb_append1 (struct string_buffer *buffer
+ int
+ sb_append_desc (struct string_buffer *buffer, string_desc_t s)
+ {
+-  size_t len = string_desc_length (s);
++  size_t len = sd_length (s);
+   if (sb_ensure_more_bytes (buffer, len) < 0)
+     {
+       buffer->error = true;
+       return -1;
+     }
+-  memcpy (buffer->data + buffer->length, string_desc_data (s), len);
++  memcpy (buffer->data + buffer->length, sd_data (s), len);
+   buffer->length += len;
+   return 0;
+ }
+@@ -136,7 +136,7 @@ sb_free (struct string_buffer *buffer)
+ string_desc_t
+ sb_contents (struct string_buffer *buffer)
+ {
+-  return string_desc_new_addr (buffer->length, buffer->data);
++  return sd_new_addr (buffer->length, buffer->data);
+ }
+ 
+ const char *
+@@ -162,7 +162,7 @@ sb_dupfree (struct string_buffer *buffer
+       if (copy == NULL)
+         goto fail;
+       memcpy (copy, buffer->data, length);
+-      return string_desc_new_addr (length, copy);
++      return sd_new_addr (length, copy);
+     }
+   else
+     {
+@@ -174,12 +174,12 @@ sb_dupfree (struct string_buffer *buffer
+           if (contents == NULL)
+             goto fail;
+         }
+-      return string_desc_new_addr (length, contents);
++      return sd_new_addr (length, contents);
+     }
+ 
+  fail:
+   sb_free (buffer);
+-  return string_desc_new_addr (0, NULL);
++  return sd_new_addr (0, NULL);
+ }
+ 
+ char *
+--- a/lib/string-buffer.h
++++ b/lib/string-buffer.h
+@@ -115,7 +115,7 @@ extern const char * sb_contents_c (struc
+ 
+ /* Returns the contents of BUFFER and frees all other memory held by BUFFER.
+    Returns NULL upon failure or if there was an error earlier.
+-   It is the responsibility of the caller to string_desc_free() the result.  */
++   It is the responsibility of the caller to sd_free() the result.  */
+ extern string_desc_t sb_dupfree (struct string_buffer *buffer)
+   _GL_ATTRIBUTE_RELEASE_CAPABILITY (buffer->data);
+ 
+@@ -182,7 +182,7 @@ extern const char * sb_xcontents_c (stru
+ 
+ /* Returns the contents of BUFFER and frees all other memory held by BUFFER.
+    Returns (0, NULL) if there was an error earlier.
+-   It is the responsibility of the caller to string_desc_free() the result.  */
++   It is the responsibility of the caller to sd_free() the result.  */
+ extern string_desc_t sb_xdupfree (struct string_buffer *buffer)
+   _GL_ATTRIBUTE_RELEASE_CAPABILITY (buffer->data);
+ 
+--- a/lib/string-desc-contains.c
++++ b/lib/string-desc-contains.c
+@@ -31,7 +31,7 @@
+    which — depending on platforms — costs up to 2 KB of binary code.  */
+ 
+ ptrdiff_t
+-string_desc_contains (string_desc_t haystack, string_desc_t needle)
++sd_contains (string_desc_t haystack, string_desc_t needle)
+ {
+   if (needle._nbytes == 0)
+     return 0;
+--- a/lib/string-desc-quotearg.h
++++ b/lib/string-desc-quotearg.h
+@@ -50,22 +50,22 @@ extern "C" {
+    does not use backslash escapes and the flags of O do not request
+    elision of null bytes.  */
+ #if 0
+-extern size_t string_desc_quotearg_buffer (char *restrict buffer,
+-                                           size_t buffersize,
+-                                           string_desc_t arg,
+-                                           struct quoting_options const *o);
++extern size_t sd_quotearg_buffer (char *restrict buffer,
++                                  size_t buffersize,
++                                  string_desc_t arg,
++                                  struct quoting_options const *o);
+ #endif
+ 
+-/* Like string_desc_quotearg_buffer, except return the result in a newly
++/* Like sd_quotearg_buffer, except return the result in a newly
+    allocated buffer and store its length, excluding the terminating null
+    byte, in *SIZE.  It is the caller's responsibility to free the result.
+    The result might contain embedded null bytes if the style of O does
+    not use backslash escapes and the flags of O do not request elision
+    of null bytes.  */
+ #if 0
+-extern char *string_desc_quotearg_alloc (string_desc_t arg,
+-                                         size_t *size,
+-                                         struct quoting_options const *o)
++extern char *sd_quotearg_alloc (string_desc_t arg,
++                                size_t *size,
++                                struct quoting_options const *o)
+   _GL_ATTRIBUTE_NONNULL ((2))
+   _GL_ATTRIBUTE_MALLOC _GL_ATTRIBUTE_DEALLOC_FREE
+   _GL_ATTRIBUTE_RETURNS_NONNULL;
+@@ -77,68 +77,68 @@ extern char *string_desc_quotearg_alloc
+    reused by the next call to this function with the same value of N.
+    N must be nonnegative.  */
+ #if 0
+-extern char *string_desc_quotearg_n (int n, string_desc_t arg);
++extern char *sd_quotearg_n (int n, string_desc_t arg);
+ #endif
+ 
+-/* Equivalent to string_desc_quotearg_n (0, ARG).  */
++/* Equivalent to sd_quotearg_n (0, ARG).  */
+ #if 0
+-extern char *string_desc_quotearg (string_desc_t arg);
++extern char *sd_quotearg (string_desc_t arg);
+ #endif
+ 
+ /* Use style S and storage slot N to return a quoted version of the string ARG.
+-   This is like string_desc_quotearg_n (N, ARG), except that it uses S
++   This is like sd_quotearg_n (N, ARG), except that it uses S
+    with no other options to specify the quoting method.  */
+ #if 0
+-extern char *string_desc_quotearg_n_style (int n, enum quoting_style s,
+-                                           string_desc_t arg);
++extern char *sd_quotearg_n_style (int n, enum quoting_style s,
++                                  string_desc_t arg);
+ #endif
+ 
+-/* Equivalent to string_desc_quotearg_n_style (0, S, ARG).  */
++/* Equivalent to sd_quotearg_n_style (0, S, ARG).  */
+ #if 0
+-extern char *string_desc_quotearg_style (enum quoting_style s,
+-                                         string_desc_t arg);
++extern char *sd_quotearg_style (enum quoting_style s,
++                                string_desc_t arg);
+ #endif
+ 
+-/* Like string_desc_quotearg (ARG), except also quote any instances of CH.
++/* Like sd_quotearg (ARG), except also quote any instances of CH.
+    See set_char_quoting for a description of acceptable CH values.  */
+ #if 0
+-extern char *string_desc_quotearg_char (string_desc_t arg, char ch);
++extern char *sd_quotearg_char (string_desc_t arg, char ch);
+ #endif
+ 
+-/* Equivalent to string_desc_quotearg_char (ARG, ':').  */
++/* Equivalent to sd_quotearg_char (ARG, ':').  */
+ #if 0
+-extern char *string_desc_quotearg_colon (string_desc_t arg);
++extern char *sd_quotearg_colon (string_desc_t arg);
+ #endif
+ 
+-/* Like string_desc_quotearg_n_style (N, S, ARG) but with S as
++/* Like sd_quotearg_n_style (N, S, ARG) but with S as
+    custom_quoting_style with left quote as LEFT_QUOTE and right quote
+    as RIGHT_QUOTE.  See set_custom_quoting for a description of acceptable
+    LEFT_QUOTE and RIGHT_QUOTE values.  */
+ #if 0
+-extern char *string_desc_quotearg_n_custom (int n,
+-                                            char const *left_quote,
+-                                            char const *right_quote,
+-                                            string_desc_t arg);
++extern char *sd_quotearg_n_custom (int n,
++                                   char const *left_quote,
++                                   char const *right_quote,
++                                   string_desc_t arg);
+ #endif
+ 
+ /* Equivalent to
+-   string_desc_quotearg_n_custom (0, LEFT_QUOTE, RIGHT_QUOTE, ARG).  */
++   sd_quotearg_n_custom (0, LEFT_QUOTE, RIGHT_QUOTE, ARG).  */
+ #if 0
+-extern char *string_desc_quotearg_custom (char const *left_quote,
+-                                          char const *right_quote,
+-                                          string_desc_t arg);
++extern char *sd_quotearg_custom (char const *left_quote,
++                                 char const *right_quote,
++                                 string_desc_t arg);
+ #endif
+ 
+ 
+ /* ==== Inline function definitions ==== */
+ 
+ GL_STRING_DESC_QUOTEARG_INLINE size_t
+-string_desc_quotearg_buffer (char *restrict buffer, size_t buffersize,
+-                             string_desc_t arg,
+-                             struct quoting_options const *o)
++sd_quotearg_buffer (char *restrict buffer, size_t buffersize,
++                    string_desc_t arg,
++                    struct quoting_options const *o)
+ {
+   return quotearg_buffer (buffer, buffersize,
+-                          string_desc_data (arg), string_desc_length (arg),
++                          sd_data (arg), sd_length (arg),
+                           o);
+ }
+ 
+@@ -147,69 +147,69 @@ _GL_ATTRIBUTE_NONNULL ((2))
+ _GL_ATTRIBUTE_MALLOC _GL_ATTRIBUTE_DEALLOC_FREE
+ _GL_ATTRIBUTE_RETURNS_NONNULL
+ char *
+-string_desc_quotearg_alloc (string_desc_t arg,
+-                            size_t *size,
+-                            struct quoting_options const *o)
++sd_quotearg_alloc (string_desc_t arg,
++                   size_t *size,
++                   struct quoting_options const *o)
+ {
+-  return quotearg_alloc_mem (string_desc_data (arg), string_desc_length (arg),
++  return quotearg_alloc_mem (sd_data (arg), sd_length (arg),
+                              size,
+                              o);
+ }
+ 
+ GL_STRING_DESC_QUOTEARG_INLINE char *
+-string_desc_quotearg_n (int n, string_desc_t arg)
++sd_quotearg_n (int n, string_desc_t arg)
+ {
+-  return quotearg_n_mem (n, string_desc_data (arg), string_desc_length (arg));
++  return quotearg_n_mem (n, sd_data (arg), sd_length (arg));
+ }
+ 
+ GL_STRING_DESC_QUOTEARG_INLINE char *
+-string_desc_quotearg (string_desc_t arg)
++sd_quotearg (string_desc_t arg)
+ {
+-  return quotearg_mem (string_desc_data (arg), string_desc_length (arg));
++  return quotearg_mem (sd_data (arg), sd_length (arg));
+ }
+ 
+ GL_STRING_DESC_QUOTEARG_INLINE char *
+-string_desc_quotearg_n_style (int n, enum quoting_style s, string_desc_t arg)
++sd_quotearg_n_style (int n, enum quoting_style s, string_desc_t arg)
+ {
+   return quotearg_n_style_mem (n, s,
+-                               string_desc_data (arg), string_desc_length (arg));
++                               sd_data (arg), sd_length (arg));
+ }
+ 
+ GL_STRING_DESC_QUOTEARG_INLINE char *
+-string_desc_quotearg_style (enum quoting_style s, string_desc_t arg)
++sd_quotearg_style (enum quoting_style s, string_desc_t arg)
+ {
+   return quotearg_style_mem (s,
+-                             string_desc_data (arg), string_desc_length (arg));
++                             sd_data (arg), sd_length (arg));
+ }
+ 
+ GL_STRING_DESC_QUOTEARG_INLINE char *
+-string_desc_quotearg_char (string_desc_t arg, char ch)
++sd_quotearg_char (string_desc_t arg, char ch)
+ {
+-  return quotearg_char_mem (string_desc_data (arg), string_desc_length (arg),
++  return quotearg_char_mem (sd_data (arg), sd_length (arg),
+                             ch);
+ }
+ 
+ GL_STRING_DESC_QUOTEARG_INLINE char *
+-string_desc_quotearg_colon (string_desc_t arg)
++sd_quotearg_colon (string_desc_t arg)
+ {
+-  return quotearg_colon_mem (string_desc_data (arg), string_desc_length (arg));
++  return quotearg_colon_mem (sd_data (arg), sd_length (arg));
+ }
+ 
+ GL_STRING_DESC_QUOTEARG_INLINE char *
+-string_desc_quotearg_n_custom (int n,
+-                               char const *left_quote, char const *right_quote,
+-                               string_desc_t arg)
++sd_quotearg_n_custom (int n,
++                      char const *left_quote, char const *right_quote,
++                      string_desc_t arg)
+ {
+   return quotearg_n_custom_mem (n, left_quote, right_quote,
+-                                string_desc_data (arg), string_desc_length (arg));
++                                sd_data (arg), sd_length (arg));
+ }
+ 
+ GL_STRING_DESC_QUOTEARG_INLINE char *
+-string_desc_quotearg_custom (char const *left_quote, char const *right_quote,
+-                             string_desc_t arg)
++sd_quotearg_custom (char const *left_quote, char const *right_quote,
++                    string_desc_t arg)
+ {
+   return quotearg_custom_mem (left_quote, right_quote,
+-                              string_desc_data (arg), string_desc_length (arg));
++                              sd_data (arg), sd_length (arg));
+ }
+ 
+ 
+--- a/lib/string-desc.c
++++ b/lib/string-desc.c
+@@ -39,14 +39,14 @@
+ 
+ /* Return true if A and B are equal.  */
+ bool
+-string_desc_equals (string_desc_t a, string_desc_t b)
++sd_equals (string_desc_t a, string_desc_t b)
+ {
+   return (a._nbytes == b._nbytes
+           && (a._nbytes == 0 || memcmp (a._data, b._data, a._nbytes) == 0));
+ }
+ 
+ bool
+-string_desc_startswith (string_desc_t s, string_desc_t prefix)
++sd_startswith (string_desc_t s, string_desc_t prefix)
+ {
+   return (s._nbytes >= prefix._nbytes
+           && (prefix._nbytes == 0
+@@ -54,7 +54,7 @@ string_desc_startswith (string_desc_t s,
+ }
+ 
+ bool
+-string_desc_endswith (string_desc_t s, string_desc_t suffix)
++sd_endswith (string_desc_t s, string_desc_t suffix)
+ {
+   return (s._nbytes >= suffix._nbytes
+           && (suffix._nbytes == 0
+@@ -63,7 +63,7 @@ string_desc_endswith (string_desc_t s, s
+ }
+ 
+ int
+-string_desc_cmp (string_desc_t a, string_desc_t b)
++sd_cmp (string_desc_t a, string_desc_t b)
+ {
+   if (a._nbytes > b._nbytes)
+     {
+@@ -86,14 +86,14 @@ string_desc_cmp (string_desc_t a, string
+ }
+ 
+ int
+-string_desc_c_casecmp (string_desc_t a, string_desc_t b)
++sd_c_casecmp (string_desc_t a, string_desc_t b)
+ {
+   /* Don't use memcasecmp here, since it uses the current locale, not the
+      "C" locale.  */
+-  idx_t an = string_desc_length (a);
+-  idx_t bn = string_desc_length (b);
+-  const char *ap = string_desc_data (a);
+-  const char *bp = string_desc_data (b);
++  idx_t an = sd_length (a);
++  idx_t bn = sd_length (b);
++  const char *ap = sd_data (a);
++  const char *bp = sd_data (b);
+   idx_t n = (an < bn ? an : bn);
+   idx_t i;
+   for (i = 0; i < n; i++)
+@@ -108,7 +108,7 @@ string_desc_c_casecmp (string_desc_t a,
+ }
+ 
+ ptrdiff_t
+-string_desc_index (string_desc_t s, char c)
++sd_index (string_desc_t s, char c)
+ {
+   if (s._nbytes > 0)
+     {
+@@ -120,7 +120,7 @@ string_desc_index (string_desc_t s, char
+ }
+ 
+ ptrdiff_t
+-string_desc_last_index (string_desc_t s, char c)
++sd_last_index (string_desc_t s, char c)
+ {
+   if (s._nbytes > 0)
+     {
+@@ -132,7 +132,7 @@ string_desc_last_index (string_desc_t s,
+ }
+ 
+ string_desc_t
+-string_desc_new_empty (void)
++sd_new_empty (void)
+ {
+   string_desc_t result;
+ 
+@@ -144,7 +144,7 @@ string_desc_new_empty (void)
+ }
+ 
+ string_desc_t
+-string_desc_new_addr (idx_t n, char *addr)
++sd_new_addr (idx_t n, char *addr)
+ {
+   string_desc_t result;
+ 
+@@ -158,7 +158,7 @@ string_desc_new_addr (idx_t n, char *add
+ }
+ 
+ string_desc_t
+-string_desc_from_c (const char *s)
++sd_from_c (const char *s)
+ {
+   string_desc_t result;
+ 
+@@ -169,7 +169,7 @@ string_desc_from_c (const char *s)
+ }
+ 
+ string_desc_t
+-string_desc_substring (string_desc_t s, idx_t start, idx_t end)
++sd_substring (string_desc_t s, idx_t start, idx_t end)
+ {
+   string_desc_t result;
+ 
+@@ -184,7 +184,7 @@ string_desc_substring (string_desc_t s,
+ }
+ 
+ int
+-string_desc_write (int fd, string_desc_t s)
++sd_write (int fd, string_desc_t s)
+ {
+   if (s._nbytes > 0)
+     if (full_write (fd, s._data, s._nbytes) != s._nbytes)
+@@ -194,7 +194,7 @@ string_desc_write (int fd, string_desc_t
+ }
+ 
+ int
+-string_desc_fwrite (FILE *fp, string_desc_t s)
++sd_fwrite (FILE *fp, string_desc_t s)
+ {
+   if (s._nbytes > 0)
+     if (fwrite (s._data, 1, s._nbytes, fp) != s._nbytes)
+@@ -206,7 +206,7 @@ string_desc_fwrite (FILE *fp, string_des
+ /* ==== Memory-allocating operations on string descriptors ==== */
+ 
+ int
+-string_desc_new (string_desc_t *resultp, idx_t n)
++sd_new (string_desc_t *resultp, idx_t n)
+ {
+   string_desc_t result;
+ 
+@@ -230,7 +230,7 @@ string_desc_new (string_desc_t *resultp,
+ }
+ 
+ int
+-string_desc_new_filled (string_desc_t *resultp, idx_t n, char c)
++sd_new_filled (string_desc_t *resultp, idx_t n, char c)
+ {
+   string_desc_t result;
+ 
+@@ -251,7 +251,7 @@ string_desc_new_filled (string_desc_t *r
+ }
+ 
+ int
+-string_desc_copy (string_desc_t *resultp, string_desc_t s)
++sd_copy (string_desc_t *resultp, string_desc_t s)
+ {
+   string_desc_t result;
+   idx_t n = s._nbytes;
+@@ -273,7 +273,7 @@ string_desc_copy (string_desc_t *resultp
+ }
+ 
+ int
+-string_desc_concat (string_desc_t *resultp, idx_t n, string_desc_t string1, ...)
++sd_concat (string_desc_t *resultp, idx_t n, string_desc_t string1, ...)
+ {
+   if (n <= 0)
+     /* Invalid argument.  */
+@@ -327,7 +327,7 @@ string_desc_concat (string_desc_t *resul
+ }
+ 
+ char *
+-string_desc_c (string_desc_t s)
++sd_c (string_desc_t s)
+ {
+   idx_t n = s._nbytes;
+   char *result = (char *) imalloc (n + 1);
+@@ -345,7 +345,7 @@ string_desc_c (string_desc_t s)
+ /* ==== Operations with side effects on string descriptors ==== */
+ 
+ void
+-string_desc_set_char_at (string_desc_t s, idx_t i, char c)
++sd_set_char_at (string_desc_t s, idx_t i, char c)
+ {
+   if (!(i >= 0 && i < s._nbytes))
+     /* Invalid argument.  */
+@@ -354,7 +354,7 @@ string_desc_set_char_at (string_desc_t s
+ }
+ 
+ void
+-string_desc_fill (string_desc_t s, idx_t start, idx_t end, char c)
++sd_fill (string_desc_t s, idx_t start, idx_t end, char c)
+ {
+   if (!(start >= 0 && start <= end))
+     /* Invalid arguments.  */
+@@ -365,7 +365,7 @@ string_desc_fill (string_desc_t s, idx_t
+ }
+ 
+ void
+-string_desc_overwrite (string_desc_t s, idx_t start, string_desc_t t)
++sd_overwrite (string_desc_t s, idx_t start, string_desc_t t)
+ {
+   if (!(start >= 0 && start + t._nbytes <= s._nbytes))
+     /* Invalid arguments.  */
+@@ -376,7 +376,7 @@ string_desc_overwrite (string_desc_t s,
+ }
+ 
+ void
+-string_desc_free (string_desc_t s)
++sd_free (string_desc_t s)
+ {
+   free (s._data);
+ }
+--- a/lib/string-desc.h
++++ b/lib/string-desc.h
+@@ -69,82 +69,82 @@ struct string_desc_t
+ 
+ /* Return the length of the string S.  */
+ #if 0 /* Defined inline below.  */
+-extern idx_t string_desc_length (string_desc_t s);
++extern idx_t sd_length (string_desc_t s);
+ #endif
+ 
+ /* Return the byte at index I of string S.
+    I must be < length(S).  */
+ #if 0 /* Defined inline below.  */
+-extern char string_desc_char_at (string_desc_t s, idx_t i);
++extern char sd_char_at (string_desc_t s, idx_t i);
+ #endif
+ 
+ /* Return a read-only view of the bytes of S.  */
+ #if 0 /* Defined inline below.  */
+-extern const char * string_desc_data (string_desc_t s);
++extern const char * sd_data (string_desc_t s);
+ #endif
+ 
+ /* Return true if S is the empty string.  */
+ #if 0 /* Defined inline below.  */
+-extern bool string_desc_is_empty (string_desc_t s);
++extern bool sd_is_empty (string_desc_t s);
+ #endif
+ 
+ /* Return true if A and B are equal.  */
+-extern bool string_desc_equals (string_desc_t a, string_desc_t b);
++extern bool sd_equals (string_desc_t a, string_desc_t b);
+ 
+ /* Return true if S starts with PREFIX.  */
+-extern bool string_desc_startswith (string_desc_t s, string_desc_t prefix);
++extern bool sd_startswith (string_desc_t s, string_desc_t prefix);
+ 
+ /* Return true if S ends with SUFFIX.  */
+-extern bool string_desc_endswith (string_desc_t s, string_desc_t suffix);
++extern bool sd_endswith (string_desc_t s, string_desc_t suffix);
+ 
+ /* Return > 0, == 0, or < 0 if A > B, A == B, A < B.
+    This uses a lexicographic ordering, where the bytes are compared as
+    'unsigned char'.  */
+-extern int string_desc_cmp (string_desc_t a, string_desc_t b);
++extern int sd_cmp (string_desc_t a, string_desc_t b);
+ 
+ /* Return > 0, == 0, or < 0 if A > B, A == B, A < B.
+    Either A or B must be entirely ASCII.
+    This uses a lexicographic ordering, where the bytes are compared as
+    'unsigned char', ignoring case, in the "C" locale.  */
+-extern int string_desc_c_casecmp (string_desc_t a, string_desc_t b);
++extern int sd_c_casecmp (string_desc_t a, string_desc_t b);
+ 
+ /* Return the index of the first occurrence of C in S,
+    or -1 if there is none.  */
+-extern ptrdiff_t string_desc_index (string_desc_t s, char c);
++extern ptrdiff_t sd_index (string_desc_t s, char c);
+ 
+ /* Return the index of the last occurrence of C in S,
+    or -1 if there is none.  */
+-extern ptrdiff_t string_desc_last_index (string_desc_t s, char c);
++extern ptrdiff_t sd_last_index (string_desc_t s, char c);
+ 
+ /* Return the index of the first occurrence of NEEDLE in HAYSTACK,
+    or -1 if there is none.  */
+-extern ptrdiff_t string_desc_contains (string_desc_t haystack, string_desc_t needle);
++extern ptrdiff_t sd_contains (string_desc_t haystack, string_desc_t needle);
+ 
+ /* Return an empty string.  */
+-extern string_desc_t string_desc_new_empty (void);
++extern string_desc_t sd_new_empty (void);
+ 
+ /* Construct and return a string of length N, at the given memory address.  */
+-extern string_desc_t string_desc_new_addr (idx_t n, char *addr);
++extern string_desc_t sd_new_addr (idx_t n, char *addr);
+ 
+ /* Return a string that represents the C string S, of length strlen (S).  */
+-extern string_desc_t string_desc_from_c (const char *s);
++extern string_desc_t sd_from_c (const char *s);
+ 
+ /* Return the substring of S, starting at offset START and ending at offset END.
+    START must be <= END.
+    The result is of length END - START.
+    The result must not be freed (since its storage is part of the storage
+    of S).  */
+-extern string_desc_t string_desc_substring (string_desc_t s, idx_t start, idx_t end);
++extern string_desc_t sd_substring (string_desc_t s, idx_t start, idx_t end);
+ 
+ /* Output S to the file descriptor FD.
+    Return 0 if successful.
+    Upon error, return -1 with errno set.  */
+-extern int string_desc_write (int fd, string_desc_t s);
++extern int sd_write (int fd, string_desc_t s);
+ 
+ /* Output S to the FILE stream FP.
+    Return 0 if successful.
+    Upon error, return -1.  */
+-extern int string_desc_fwrite (FILE *fp, string_desc_t s);
++extern int sd_fwrite (FILE *fp, string_desc_t s);
+ 
+ 
+ /* ==== Memory-allocating operations on string descriptors ==== */
+@@ -153,61 +153,61 @@ extern int string_desc_fwrite (FILE *fp,
+    Return 0 if successful.
+    Upon error, return -1 with errno set.  */
+ _GL_ATTRIBUTE_NODISCARD
+-extern int string_desc_new (string_desc_t *resultp, idx_t n);
++extern int sd_new (string_desc_t *resultp, idx_t n);
+ 
+ /* Construct a string of length N, filled with C.
+    Return 0 if successful.
+    Upon error, return -1 with errno set.  */
+ _GL_ATTRIBUTE_NODISCARD
+-extern int string_desc_new_filled (string_desc_t *resultp, idx_t n, char c);
++extern int sd_new_filled (string_desc_t *resultp, idx_t n, char c);
+ 
+ /* Construct a copy of string S.
+    Return 0 if successful.
+    Upon error, return -1 with errno set.  */
+ _GL_ATTRIBUTE_NODISCARD
+-extern int string_desc_copy (string_desc_t *resultp, string_desc_t s);
++extern int sd_copy (string_desc_t *resultp, string_desc_t s);
+ 
+ /* Construct the concatenation of N strings.  N must be > 0.
+    Return 0 if successful.
+    Upon error, return -1 with errno set.  */
+ _GL_ATTRIBUTE_NODISCARD
+-extern int string_desc_concat (string_desc_t *resultp, idx_t n, string_desc_t string1, ...);
++extern int sd_concat (string_desc_t *resultp, idx_t n, string_desc_t string1, ...);
+ 
+ /* Construct a copy of string S, as a NUL-terminated C string.
+    Return it is successful.
+    Upon error, return NULL with errno set.  */
+-extern char * string_desc_c (string_desc_t s) _GL_ATTRIBUTE_DEALLOC_FREE;
++extern char * sd_c (string_desc_t s) _GL_ATTRIBUTE_DEALLOC_FREE;
+ 
+ 
+ /* ==== Operations with side effects on string descriptors ==== */
+ 
+ /* Overwrite the byte at index I of string S with C.
+    I must be < length(S).  */
+-extern void string_desc_set_char_at (string_desc_t s, idx_t i, char c);
++extern void sd_set_char_at (string_desc_t s, idx_t i, char c);
+ 
+ /* Fill part of S, starting at offset START and ending at offset END,
+    with copies of C.
+    START must be <= END.  */
+-extern void string_desc_fill (string_desc_t s, idx_t start, idx_t end, char c);
++extern void sd_fill (string_desc_t s, idx_t start, idx_t end, char c);
+ 
+ /* Overwrite part of S with T, starting at offset START.
+    START + length(T) must be <= length (S).  */
+-extern void string_desc_overwrite (string_desc_t s, idx_t start, string_desc_t t);
++extern void sd_overwrite (string_desc_t s, idx_t start, string_desc_t t);
+ 
+ /* Free S.  */
+-extern void string_desc_free (string_desc_t s);
++extern void sd_free (string_desc_t s);
+ 
+ 
+ /* ==== Inline function definitions ==== */
+ 
+ GL_STRING_DESC_INLINE idx_t
+-string_desc_length (string_desc_t s)
++sd_length (string_desc_t s)
+ {
+   return s._nbytes;
+ }
+ 
+ GL_STRING_DESC_INLINE char
+-string_desc_char_at (string_desc_t s, idx_t i)
++sd_char_at (string_desc_t s, idx_t i)
+ {
+   if (!(i >= 0 && i < s._nbytes))
+     /* Invalid argument.  */
+@@ -216,13 +216,13 @@ string_desc_char_at (string_desc_t s, id
+ }
+ 
+ GL_STRING_DESC_INLINE const char *
+-string_desc_data (string_desc_t s)
++sd_data (string_desc_t s)
+ {
+   return s._data;
+ }
+ 
+ GL_STRING_DESC_INLINE bool
+-string_desc_is_empty (string_desc_t s)
++sd_is_empty (string_desc_t s)
+ {
+   return s._nbytes == 0;
+ }
+--- a/lib/xstring-buffer.c
++++ b/lib/xstring-buffer.c
+@@ -59,10 +59,10 @@ sb_xdupfree (struct string_buffer *buffe
+   if (buffer->error)
+     {
+       sb_free (buffer);
+-      return string_desc_new_addr (0, NULL);
++      return sd_new_addr (0, NULL);
+     }
+   string_desc_t contents = sb_dupfree (buffer);
+-  if (string_desc_data (contents) == NULL)
++  if (sd_data (contents) == NULL)
+     xalloc_die ();
+   return contents;
+ }
+--- a/lib/xstring-desc.c
++++ b/lib/xstring-desc.c
+@@ -22,7 +22,7 @@
+ #include "ialloc.h"
+ 
+ string_desc_t
+-xstring_desc_concat (idx_t n, string_desc_t string1, ...)
++xsd_concat (idx_t n, string_desc_t string1, ...)
+ {
+   if (n <= 0)
+     /* Invalid argument.  */
+--- a/lib/xstring-desc.h
++++ b/lib/xstring-desc.h
+@@ -43,53 +43,53 @@ extern "C" {
+ 
+ /* Return a string of length N, with uninitialized contents.  */
+ #if 0 /* Defined inline below.  */
+-extern string_desc_t xstring_desc_new (idx_t n);
++extern string_desc_t xsd_new (idx_t n);
+ #endif
+ 
+ /* Return a string of length N, filled with C.  */
+ #if 0 /* Defined inline below.  */
+-extern string_desc_t xstring_desc_new_filled (idx_t n, char c);
++extern string_desc_t xsd_new_filled (idx_t n, char c);
+ #endif
+ 
+ /* Return a copy of string S.  */
+ #if 0 /* Defined inline below.  */
+-extern string_desc_t xstring_desc_copy (string_desc_t s);
++extern string_desc_t xsd_copy (string_desc_t s);
+ #endif
+ 
+ /* Return the concatenation of N strings.  N must be > 0.  */
+-extern string_desc_t xstring_desc_concat (idx_t n, string_desc_t string1, ...);
++extern string_desc_t xsd_concat (idx_t n, string_desc_t string1, ...);
+ 
+ /* Construct and return a copy of string S, as a NUL-terminated C string.  */
+ #if 0 /* Defined inline below.  */
+-extern char * xstring_desc_c (string_desc_t s) _GL_ATTRIBUTE_DEALLOC_FREE;
++extern char * xsd_c (string_desc_t s) _GL_ATTRIBUTE_DEALLOC_FREE;
+ #endif
+ 
+ 
+ /* ==== Inline function definitions ==== */
+ 
+ GL_XSTRING_DESC_INLINE string_desc_t
+-xstring_desc_new (idx_t n)
++xsd_new (idx_t n)
+ {
+   string_desc_t result;
+-  if (string_desc_new (&result, n) < 0)
++  if (sd_new (&result, n) < 0)
+     xalloc_die ();
+   return result;
+ }
+ 
+ GL_XSTRING_DESC_INLINE string_desc_t
+-xstring_desc_new_filled (idx_t n, char c)
++xsd_new_filled (idx_t n, char c)
+ {
+   string_desc_t result;
+-  if (string_desc_new_filled (&result, n, c) < 0)
++  if (sd_new_filled (&result, n, c) < 0)
+     xalloc_die ();
+   return result;
+ }
+ 
+ GL_XSTRING_DESC_INLINE string_desc_t
+-xstring_desc_copy (string_desc_t s)
++xsd_copy (string_desc_t s)
+ {
+   string_desc_t result;
+-  if (string_desc_copy (&result, s) < 0)
++  if (sd_copy (&result, s) < 0)
+     xalloc_die ();
+   return result;
+ }
+@@ -97,9 +97,9 @@ xstring_desc_copy (string_desc_t s)
+ GL_XSTRING_DESC_INLINE
+ _GL_ATTRIBUTE_DEALLOC_FREE
+ char *
+-xstring_desc_c (string_desc_t s)
++xsd_c (string_desc_t s)
+ {
+-  char *result = string_desc_c (s);
++  char *result = sd_c (s);
+   if (result == NULL)
+     xalloc_die ();
+   return result;
+--- a/tests/test-string-desc-quotearg.c
++++ b/tests/test-string-desc-quotearg.c
+@@ -28,75 +28,75 @@
+ int
+ main (void)
+ {
+-  string_desc_t s1 = string_desc_from_c ("Hello world!");
+-  string_desc_t s2 = string_desc_new_addr (21, "The\0quick\0brown\0\0fox");
++  string_desc_t s1 = sd_from_c ("Hello world!");
++  string_desc_t s2 = sd_new_addr (21, "The\0quick\0brown\0\0fox");
+ 
+-  /* Test string_desc_quotearg_buffer.  */
++  /* Test sd_quotearg_buffer.  */
+   {
+     char buf[80];
+-    size_t n = string_desc_quotearg_buffer (buf, sizeof (buf), s2, NULL);
++    size_t n = sd_quotearg_buffer (buf, sizeof (buf), s2, NULL);
+     ASSERT (n == 21);
+     ASSERT (memcmp (buf, "The\0quick\0brown\0\0fox", n) == 0);
+   }
+ 
+-  /* Test string_desc_quotearg_alloc.  */
++  /* Test sd_quotearg_alloc.  */
+   {
+     size_t n;
+-    char *ret = string_desc_quotearg_alloc (s2, &n, NULL);
++    char *ret = sd_quotearg_alloc (s2, &n, NULL);
+     ASSERT (n == 21);
+     ASSERT (memcmp (ret, "The\0quick\0brown\0\0fox", n) == 0);
+     free (ret);
+   }
+ 
+-  /* Test string_desc_quotearg_n.  */
++  /* Test sd_quotearg_n.  */
+   {
+-    char *ret = string_desc_quotearg_n (1, s2);
++    char *ret = sd_quotearg_n (1, s2);
+     ASSERT (memcmp (ret, "Thequickbrownfox", 16 + 1) == 0);
+   }
+ 
+-  /* Test string_desc_quotearg.  */
++  /* Test sd_quotearg.  */
+   {
+-    char *ret = string_desc_quotearg (s2);
++    char *ret = sd_quotearg (s2);
+     ASSERT (memcmp (ret, "Thequickbrownfox", 16 + 1) == 0);
+   }
+ 
+-  /* Test string_desc_quotearg_n_style.  */
++  /* Test sd_quotearg_n_style.  */
+   {
+-    char *ret = string_desc_quotearg_n_style (1, clocale_quoting_style, s2);
++    char *ret = sd_quotearg_n_style (1, clocale_quoting_style, s2);
+     ASSERT (memcmp (ret, "\"The\\0quick\\0brown\\0\\0fox\\0\"", 28 + 1) == 0
+             || /* if the locale has UTF-8 encoding */
+                memcmp (ret, "\342\200\230The\\0quick\\0brown\\0\\0fox\\0\342\200\231", 32 + 1) == 0);
+   }
+ 
+-  /* Test string_desc_quotearg_style.  */
++  /* Test sd_quotearg_style.  */
+   {
+-    char *ret = string_desc_quotearg_style (clocale_quoting_style, s2);
++    char *ret = sd_quotearg_style (clocale_quoting_style, s2);
+     ASSERT (memcmp (ret, "\"The\\0quick\\0brown\\0\\0fox\\0\"", 28 + 1) == 0
+             || /* if the locale has UTF-8 encoding */
+                memcmp (ret, "\342\200\230The\\0quick\\0brown\\0\\0fox\\0\342\200\231", 32 + 1) == 0);
+   }
+ 
+-  /* Test string_desc_quotearg_char.  */
++  /* Test sd_quotearg_char.  */
+   {
+-    char *ret = string_desc_quotearg_char (s1, ' ');
++    char *ret = sd_quotearg_char (s1, ' ');
+     ASSERT (memcmp (ret, "Hello world!", 12 + 1) == 0); /* ' ' not quoted?! */
+   }
+ 
+-  /* Test string_desc_quotearg_colon.  */
++  /* Test sd_quotearg_colon.  */
+   {
+-    char *ret = string_desc_quotearg_colon (string_desc_from_c ("a:b"));
++    char *ret = sd_quotearg_colon (sd_from_c ("a:b"));
+     ASSERT (memcmp (ret, "a:b", 3 + 1) == 0); /* ':' not quoted?! */
+   }
+ 
+-  /* Test string_desc_quotearg_n_custom.  */
++  /* Test sd_quotearg_n_custom.  */
+   {
+-    char *ret = string_desc_quotearg_n_custom (2, "<", ">", s1);
++    char *ret = sd_quotearg_n_custom (2, "<", ">", s1);
+     ASSERT (memcmp (ret, "<Hello world!>", 14 + 1) == 0);
+   }
+ 
+-  /* Test string_desc_quotearg_n_custom.  */
++  /* Test sd_quotearg_n_custom.  */
+   {
+-    char *ret = string_desc_quotearg_custom ("[[", "]]", s1);
++    char *ret = sd_quotearg_custom ("[[", "]]", s1);
+     ASSERT (memcmp (ret, "[[Hello world!]]", 16 + 1) == 0);
+   }
+ 
+--- a/tests/test-string-desc.sh
++++ b/tests/test-string-desc.sh
+@@ -6,7 +6,7 @@ ${CHECKER} test-string-desc${EXEEXT} tes
+ printf 'Hello world!The\0quick\0brown\0\0fox\0' > test-string-desc.ok
+ 
+ : "${DIFF=diff}"
+-${DIFF} test-string-desc.ok test-string-desc-1.tmp || { echo "string_desc_fwrite KO" 1>&2; Exit 1; }
+-${DIFF} test-string-desc.ok test-string-desc-3.tmp || { echo "string_desc_write KO" 1>&2; Exit 1; }
++${DIFF} test-string-desc.ok test-string-desc-1.tmp || { echo "sd_fwrite KO" 1>&2; Exit 1; }
++${DIFF} test-string-desc.ok test-string-desc-3.tmp || { echo "sd_write KO" 1>&2; Exit 1; }
+ 
+ Exit 0
+--- a/tests/test-xstring-desc.c
++++ b/tests/test-xstring-desc.c
+@@ -28,53 +28,53 @@
+ int
+ main (void)
+ {
+-  string_desc_t s0 = string_desc_new_empty ();
+-  string_desc_t s1 = string_desc_from_c ("Hello world!");
+-  string_desc_t s2 = string_desc_new_addr (21, "The\0quick\0brown\0\0fox");
++  string_desc_t s0 = sd_new_empty ();
++  string_desc_t s1 = sd_from_c ("Hello world!");
++  string_desc_t s2 = sd_new_addr (21, "The\0quick\0brown\0\0fox");
+ 
+-  /* Test xstring_desc_new.  */
+-  string_desc_t s4 = xstring_desc_new (5);
+-  string_desc_set_char_at (s4, 0, 'H');
+-  string_desc_set_char_at (s4, 4, 'o');
+-  string_desc_set_char_at (s4, 1, 'e');
+-  string_desc_fill (s4, 2, 4, 'l');
+-  ASSERT (string_desc_length (s4) == 5);
+-  ASSERT (string_desc_startswith (s1, s4));
++  /* Test xsd_new.  */
++  string_desc_t s4 = xsd_new (5);
++  sd_set_char_at (s4, 0, 'H');
++  sd_set_char_at (s4, 4, 'o');
++  sd_set_char_at (s4, 1, 'e');
++  sd_fill (s4, 2, 4, 'l');
++  ASSERT (sd_length (s4) == 5);
++  ASSERT (sd_startswith (s1, s4));
+ 
+-  /* Test xstring_desc_new_filled.  */
+-  string_desc_t s5 = xstring_desc_new_filled (5, 'l');
+-  string_desc_set_char_at (s5, 0, 'H');
+-  string_desc_set_char_at (s5, 4, 'o');
+-  string_desc_set_char_at (s5, 1, 'e');
+-  ASSERT (string_desc_length (s5) == 5);
+-  ASSERT (string_desc_startswith (s1, s5));
++  /* Test xsd_new_filled.  */
++  string_desc_t s5 = xsd_new_filled (5, 'l');
++  sd_set_char_at (s5, 0, 'H');
++  sd_set_char_at (s5, 4, 'o');
++  sd_set_char_at (s5, 1, 'e');
++  ASSERT (sd_length (s5) == 5);
++  ASSERT (sd_startswith (s1, s5));
+ 
+-  /* Test xstring_desc_copy.  */
++  /* Test xsd_copy.  */
+   {
+-    string_desc_t s6 = xstring_desc_copy (s0);
+-    ASSERT (string_desc_is_empty (s6));
+-    string_desc_free (s6);
++    string_desc_t s6 = xsd_copy (s0);
++    ASSERT (sd_is_empty (s6));
++    sd_free (s6);
+   }
+   {
+-    string_desc_t s6 = xstring_desc_copy (s2);
+-    ASSERT (string_desc_equals (s6, s2));
+-    string_desc_free (s6);
++    string_desc_t s6 = xsd_copy (s2);
++    ASSERT (sd_equals (s6, s2));
++    sd_free (s6);
+   }
+ 
+-  /* Test xstring_desc_concat.  */
++  /* Test xsd_concat.  */
+   {
+     string_desc_t s8 =
+-      xstring_desc_concat (3, string_desc_new_addr (10, "The\0quick"),
+-                              string_desc_new_addr (7, "brown\0"),
+-                              string_desc_new_addr (4, "fox"),
+-                              string_desc_new_addr (7, "unused"));
+-    ASSERT (string_desc_equals (s8, s2));
+-    string_desc_free (s8);
++      xsd_concat (3, sd_new_addr (10, "The\0quick"),
++                     sd_new_addr (7, "brown\0"),
++                     sd_new_addr (4, "fox"),
++                     sd_new_addr (7, "unused"));
++    ASSERT (sd_equals (s8, s2));
++    sd_free (s8);
+   }
+ 
+-  /* Test xstring_desc_c.  */
++  /* Test xsd_c.  */
+   {
+-    char *ptr = xstring_desc_c (s2);
++    char *ptr = xsd_c (s2);
+     ASSERT (ptr != NULL);
+     ASSERT (memcmp (ptr, "The\0quick\0brown\0\0fox\0", 22) == 0);
+     free (ptr);
diff --git a/tools/gnulib/patches/796-vc-mtime-less-read.patch b/tools/gnulib/patches/796-vc-mtime-less-read.patch
new file mode 100644
index 0000000000..3fabe10369
--- /dev/null
+++ b/tools/gnulib/patches/796-vc-mtime-less-read.patch
@@ -0,0 +1,44 @@
+From 60cd34886c2c9f509974239fcf64a61f9a507d14 Mon Sep 17 00:00:00 2001
+From: Bruno Haible <bruno at clisp.org>
+Date: Tue, 25 Feb 2025 09:04:28 +0100
+Subject: [PATCH] vc-mtime: Reduce number of read() system calls.
+
+* lib/vc-mtime.c: Include <stddef.h>.
+(git_vc_controlled): Read bytes into a buffer, not one-by-one.
+---
+ ChangeLog      |  6 ++++++
+ lib/vc-mtime.c | 15 +++++++++++----
+ 2 files changed, 17 insertions(+), 4 deletions(-)
+
+--- a/lib/vc-mtime.c
++++ b/lib/vc-mtime.c
+@@ -21,6 +21,7 @@
+ /* Specification.  */
+ #include "vc-mtime.h"
+ 
++#include <stddef.h>
+ #include <stdlib.h>
+ #include <unistd.h>
+ 
+@@ -56,11 +57,17 @@ git_vc_controlled (const char *filename)
+     return false;
+ 
+   /* Read the subprocess output, and test whether it is non-empty.  */
+-  size_t count = 0;
+-  char c;
++  ptrdiff_t count = 0;
+ 
+-  while (safe_read (fd[0], &c, 1) > 0)
+-    count++;
++  for (;;)
++    {
++      char buf[1024];
++      ptrdiff_t n = safe_read (fd[0], buf, sizeof (buf));
++      if (n > 0)
++        count += n;
++      else
++        break;
++    }
+ 
+   close (fd[0]);
+ 
diff --git a/tools/gnulib/patches/797-vc-mtime-add-api.patch b/tools/gnulib/patches/797-vc-mtime-add-api.patch
new file mode 100644
index 0000000000..eeb6636e67
--- /dev/null
+++ b/tools/gnulib/patches/797-vc-mtime-add-api.patch
@@ -0,0 +1,968 @@
+From 78269749030dde23182c29376d1410592436eb5d Mon Sep 17 00:00:00 2001
+From: Bruno Haible <bruno at clisp.org>
+Date: Thu, 1 May 2025 17:26:27 +0200
+Subject: [PATCH] vc-mtime: Add API for more efficient use of git.
+
+Reported by Serhii Tereshchenko, Arthur, Adam YS, Foucauld Degeorges
+at <https://savannah.gnu.org/bugs/?66865>.
+
+* lib/vc-mtime.h (max_vc_mtime): New declaration.
+* lib/vc-mtime.c: Include <errno.h>, <stdio.h>, <string.h>, filename.h,
+xalloc.h, xgetcwd.h, xvasprintf.h, gl_map.h, gl_xmap.h, gl_hash_map.h,
+hashkey-string.h, unlocked-io.h.
+(is_git_present): New function, extracted from vc_mtime.
+(vc_mtime): Invoke it.
+(MAX_COMMAND_LENGTH, MAX_CMD_LEN): New macros.
+(abs_git_checkout): New function, based on execute_and_read_line in
+lib/javacomp.c.
+(ancestor_level, relativize): New functions.
+(struct accumulator): New type.
+(accumulate): New function.
+(max_vc_mtime): New function.
+(test_ancestor_level, test_relativize, main) [TEST]: New functions.
+* modules/vc-mtime (Depends-on): Add filename, xalloc, xgetcwd,
+canonicalize-lgpl, xvasprintf, str_startswith, map, xmap, hash-map,
+hashkey-string, getdelim.
+---
+ ChangeLog        |  23 ++
+ lib/vc-mtime.c   | 866 +++++++++++++++++++++++++++++++++++++++++++++--
+ lib/vc-mtime.h   |   7 +
+ modules/vc-mtime |  11 +
+ 4 files changed, 886 insertions(+), 21 deletions(-)
+
+--- a/lib/vc-mtime.c
++++ b/lib/vc-mtime.c
+@@ -21,8 +21,11 @@
+ /* Specification.  */
+ #include "vc-mtime.h"
+ 
++#include <errno.h>
+ #include <stddef.h>
++#include <stdio.h>
+ #include <stdlib.h>
++#include <string.h>
+ #include <unistd.h>
+ 
+ #include <error.h>
+@@ -32,11 +35,51 @@
+ #include "safe-read.h"
+ #include "xstrtol.h"
+ #include "stat-time.h"
++#include "filename.h"
++#include "xalloc.h"
++#include "xgetcwd.h"
++#include "xvasprintf.h"
++#include "gl_map.h"
++#include "gl_xmap.h"
++#include "gl_hash_map.h"
++#include "hashkey-string.h"
++#if USE_UNLOCKED_IO
++# include "unlocked-io.h"
++#endif
+ #include "gettext.h"
+ 
+ #define _(msgid) dgettext ("gnulib", msgid)
+ 
+ 
++/* ========================================================================== */
++
++/* Determines whether git is present.  */
++static bool
++is_git_present (void)
++{
++  static bool git_tested;
++  static bool git_present;
++
++  if (!git_tested)
++    {
++      /* Test for presence of git:
++         "git --version >/dev/null 2>/dev/null"  */
++      const char *argv[3];
++      int exitstatus;
++
++      argv[0] = "git";
++      argv[1] = "--version";
++      argv[2] = NULL;
++      exitstatus = execute ("git", "git", argv, NULL, NULL,
++                            false, false, true, true,
++                            true, false, NULL);
++      git_present = (exitstatus == 0);
++      git_tested = true;
++    }
++
++  return git_present;
++}
++
+ /* Determines whether the specified file is under version control.  */
+ static bool
+ git_vc_controlled (const char *filename)
+@@ -178,27 +221,7 @@ git_mtime (struct timespec *mtime, const
+ int
+ vc_mtime (struct timespec *mtime, const char *filename)
+ {
+-  static bool git_tested;
+-  static bool git_present;
+-
+-  if (!git_tested)
+-    {
+-      /* Test for presence of git:
+-         "git --version >/dev/null 2>/dev/null"  */
+-      const char *argv[3];
+-      int exitstatus;
+-
+-      argv[0] = "git";
+-      argv[1] = "--version";
+-      argv[2] = NULL;
+-      exitstatus = execute ("git", "git", argv, NULL, NULL,
+-                            false, false, true, true,
+-                            true, false, NULL);
+-      git_present = (exitstatus == 0);
+-      git_tested = true;
+-    }
+-
+-  if (git_present
++  if (is_git_present ()
+       && git_vc_controlled (filename)
+       && git_unmodified (filename))
+     {
+@@ -213,3 +236,804 @@ vc_mtime (struct timespec *mtime, const
+     }
+   return -1;
+ }
++
++/* ========================================================================== */
++
++/* Maximum length of a command that is guaranteed to work.  */
++#if defined _WIN32 || defined __CYGWIN__
++/* Windows */
++# define MAX_COMMAND_LENGTH 8192
++#else
++/* Unix platforms */
++# define MAX_COMMAND_LENGTH 32768
++#endif
++/* Keep some safe distance to this maximum.  */
++#define MAX_CMD_LEN ((int) (MAX_COMMAND_LENGTH * 0.8))
++
++/* Returns the directory name of the git checkout that contains tha current
++   directory, as an absolute file name, or NULL if the current directory is
++   not in a git checkout.  */
++static char *
++abs_git_checkout (void)
++{
++  /* Run "git rev-parse --show-toplevel 2>/dev/null" and return its output,
++     without the trailing newline.  */
++  const char *argv[4];
++  pid_t child;
++  int fd[1];
++
++  argv[0] = "git";
++  argv[1] = "rev-parse";
++  argv[2] = "--show-toplevel";
++  argv[3] = NULL;
++  child = create_pipe_in ("git", "git", argv, NULL, NULL,
++                          DEV_NULL, true, true, false, fd);
++
++  if (child == -1)
++    return NULL;
++
++  /* Retrieve its result.  */
++  FILE *fp = fdopen (fd[0], "r");
++  if (fp == NULL)
++    error (EXIT_FAILURE, errno, _("fdopen() failed"));
++
++  char *line = NULL;
++  size_t linesize = 0;
++  size_t linelen = getline (&line, &linesize, fp);
++  if (linelen == (size_t)(-1))
++    {
++      fclose (fp);
++      wait_subprocess (child, "git", true, true, true, false, NULL);
++      return NULL;
++    }
++  else
++    {
++      int exitstatus;
++
++      if (linelen > 0 && line[linelen - 1] == '\n')
++        line[linelen - 1] = '\0';
++
++      /* Read until EOF (otherwise the child process may get a SIGPIPE signal).  */
++      while (getc (fp) != EOF)
++        ;
++
++      fclose (fp);
++
++      /* Remove zombie process from process list, and retrieve exit status.  */
++      exitstatus =
++        wait_subprocess (child, "git", true, true, true, false, NULL);
++      if (exitstatus == 0)
++        return line;
++    }
++  free (line);
++  return NULL;
++}
++
++/* Given an absolute canonicalized directory DIR1 and an absolute canonicalized
++   directory DIR2, returns N where DIR1 = DIR2 "/.." ... "/.." with N times
++   "/..", or -1 if DIR1 is not an ancestor directory of DIR2.  */
++static long
++ancestor_level (const char *dir1, const char *dir2)
++{
++  if (strcmp (dir1, "/") == 0)
++    dir1 = "";
++  if (strcmp (dir2, "/") == 0)
++    dir2 = "";
++  size_t dir1_len = strlen (dir1);
++  if (strncmp (dir1, dir2, dir1_len) == 0)
++    {
++      /* DIR2 starts with DIR1.  */
++      const char *p = dir2 + dir1_len;
++      if (*p == '\0')
++        /* DIR2 and DIR1 are the same.  */
++        return 0;
++      if (ISSLASH (*p))
++        {
++          /* Return the number of slashes in the tail of DIR2 that starts
++             at P.  */
++          long n = 1;
++          p++;
++          for (; *p != '\0'; p++)
++            if (ISSLASH (*p))
++              n++;
++          return n;
++        }
++    }
++  return -1;
++}
++
++/* Given an absolute canolicalized FILENAME that starts with DIR1, returns the
++   same file name relative to DIR2, where DIR1 = DIR2 "/.." ... "/.." with
++   N times "/..", as a freshly allocated string.  */
++static char *
++relativize (const char *filename,
++            unsigned long n, const char *dir1, const char *dir2)
++{
++  if (strcmp (dir1, "/") == 0)
++    dir1 = "";
++  size_t dir1_len = strlen (dir1);
++  if (!(strncmp (filename, dir1, dir1_len) == 0
++        && (filename[dir1_len] == '\0' || ISSLASH (filename[dir1_len]))))
++    /* Invalid argument.  */
++    abort ();
++  if (strcmp (dir2, "/") == 0)
++    dir2 = "";
++
++  dir2 += dir1_len;
++  filename += dir1_len;
++  for (;;)
++    {
++      /* Invariant: The result will be N times "../" followed by FILENAME.  */
++      if (*filename == '\0')
++        break;
++      if (!ISSLASH (*filename))
++        abort ();
++      filename++;
++      if (*dir2 == '\0')
++        break;
++      if (!ISSLASH (*dir2))
++        abort ();
++      dir2++;
++      /* Skip one component in DIR2.  */
++      const char *dir2_s;
++      for (dir2_s = dir2; *dir2_s != '\0'; dir2_s++)
++        if (ISSLASH (*dir2_s))
++          break;
++      /* Skip one component in FILENAME, at P.  */
++      const char *filename_s;
++      for (filename_s = filename; *filename_s != '\0'; filename_s++)
++        if (ISSLASH (*filename_s))
++          break;
++      /* Did the components match?  */
++      if (!(filename_s - filename == dir2_s - dir2
++            && memcmp (filename, dir2, dir2_s - dir2) == 0))
++        break;
++      dir2 = dir2_s;
++      filename = filename_s;
++      n--;
++    }
++
++  if (n == 0 && *filename == '\0')
++    return xstrdup (".");
++
++  char *result = (char *) xmalloc (3 * n + strlen (filename) + 1);
++  {
++    char *q = result;
++    for (; n > 0; n--)
++      {
++        q[0] = '.'; q[1] = '.'; q[2] = '/'; q += 3;
++      }
++    strcpy (q, filename);
++  }
++  return result;
++}
++
++/* Accumulating mtimes.  */
++struct accumulator
++{
++  bool has_some_mtimes;
++  struct timespec max_of_mtimes;
++};
++
++static void
++accumulate (struct accumulator *accu, struct timespec mtime)
++{
++  if (accu->has_some_mtimes)
++    {
++      /* Compute the maximum of accu->max_of_mtimes and mtime.  */
++      if (accu->max_of_mtimes.tv_sec < mtime.tv_sec
++          || (accu->max_of_mtimes.tv_sec == mtime.tv_sec
++              && accu->max_of_mtimes.tv_nsec < mtime.tv_nsec))
++       accu->max_of_mtimes = mtime;
++    }
++  else
++    {
++      accu->max_of_mtimes = mtime;
++      accu->has_some_mtimes = true;
++    }
++}
++
++int
++max_vc_mtime (struct timespec *max_of_mtimes,
++              size_t nfiles, const char * const *filenames)
++{
++  if (nfiles == 0)
++    /* Invalid argument.  */
++    abort ();
++
++  struct accumulator accu = { false };
++
++  /* Determine which of the specified files are under version control,
++     and which are duplicates.  (The case of duplicates is rare, but it needs
++     special attention, because 'git ls-files' eliminates duplicates.)
++     vc_controlled[n] = 1 means that filenames[n] is under version control.
++     vc_controlled[n] = 0 means that filenames[n] is not under version control.
++     vc_controlled[n] = -1 means that filenames[n] is a duplicate.  */
++  signed char *vc_controlled = XNMALLOC (nfiles, signed char);
++  for (size_t n = 0; n < nfiles; n++)
++    vc_controlled[n] = 0;
++
++  if (is_git_present ())
++    {
++      /* Since 'git ls-files' produces an error when at least one of the files
++         is outside the git checkout that contains tha current directory, we
++         need to filter out such files.  This is most easily done by converting
++         each file name to a canonical file name first and then comparing with
++         the directory name of said git checkout.  */
++      char *git_checkout = abs_git_checkout ();
++      if (git_checkout != NULL)
++        {
++          char *currdir = xgetcwd ();
++          /* git_checkout is expected to be an ancestor directory of the
++             current directory.  */
++          long ancestor = ancestor_level (git_checkout, currdir);
++          if (ancestor >= 0)
++            {
++              char **canonical_filenames = XNMALLOC (nfiles, char *);
++              for (size_t n = 0; n < nfiles; n++)
++                {
++                  char *canonical = canonicalize_file_name (filenames[n]);
++                  if (canonical == NULL)
++                    {
++                      if (errno == ENOMEM)
++                        xalloc_die ();
++                      /* The file filenames[n] does not exist.  */
++                      for (size_t k = n; k > 0; )
++                        free (canonical_filenames[--k]);
++                      free (canonical_filenames);
++                      free (currdir);
++                      free (git_checkout);
++                      free (vc_controlled);
++                      return -1;
++                    }
++                  canonical_filenames[n] = canonical;
++                }
++
++              /* Test which of these absolute file names are outside of the
++                 git_checkout.  */
++              char *git_checkout_slash =
++                (strcmp (git_checkout, "/") == 0
++                 ? xstrdup (git_checkout)
++                 : xasprintf ("%s/", git_checkout));
++
++              char **checkout_relative_filenames = XNMALLOC (nfiles, char *);
++              char **currdir_relative_filenames = XNMALLOC (nfiles, char *);
++              for (size_t n = 0; n < nfiles; n++)
++                {
++                  if (str_startswith (canonical_filenames[n], git_checkout_slash))
++                    {
++                      vc_controlled[n] = 1;
++                      checkout_relative_filenames[n] =
++                        relativize (canonical_filenames[n],
++                                    0, git_checkout, git_checkout);
++                      currdir_relative_filenames[n] =
++                        relativize (canonical_filenames[n],
++                                    ancestor, git_checkout, currdir);
++                    }
++                  else
++                    {
++                      vc_controlled[n] = 0;
++                      checkout_relative_filenames[n] = NULL;
++                      currdir_relative_filenames[n] = NULL;
++                    }
++                }
++
++              /* Room for passing arguments to git commands.  */
++              const char **argv = XNMALLOC (6 + nfiles + 1, const char *);
++
++              {
++                /* Put the relative file names into a hash table.  This is needed
++                   because 'git ls-files' returns the files in a different order
++                   than the one we provide in the command.  */
++                gl_map_t relative_filenames_ht =
++                  gl_map_create_empty (GL_HASH_MAP,
++                                       hashkey_string_equals, hashkey_string_hash,
++                                       NULL, NULL);
++                for (size_t n = 0; n < nfiles; n++)
++                  if (currdir_relative_filenames[n] != NULL)
++                    {
++                      if (gl_map_get (relative_filenames_ht, currdir_relative_filenames[n]) != NULL)
++                        {
++                          /* It's already in the table.  */
++                          vc_controlled[n] = -1;
++                        }
++                      else
++                        gl_map_put (relative_filenames_ht, currdir_relative_filenames[n], &vc_controlled[n]);
++                    }
++
++                /* Run "git ls-files -c -o -t -z FILE1..." for as many files as
++                   possible, and inspect the output.  */
++                size_t n0 = 0;
++                do
++                  {
++                    size_t i = 0;
++                    argv[i++] = "git";
++                    argv[i++] = "ls-files";
++                    argv[i++] = "-c";
++                    argv[i++] = "-o";
++                    argv[i++] = "-t";
++                    argv[i++] = "-z";
++                    size_t i0 = i;
++
++                    size_t n = n0;
++                    size_t cmd_len = 25;
++                    for (; n < nfiles; n++)
++                      {
++                        if (vc_controlled[n] == 1)
++                          {
++                            if (cmd_len + strlen (currdir_relative_filenames[n]) >= MAX_CMD_LEN
++                                && i > i0)
++                              break;
++                            argv[i++] = currdir_relative_filenames[n];
++                            cmd_len += 1 + strlen (currdir_relative_filenames[n]);
++                          }
++                        n++;
++                      }
++                    if (i > i0)
++                      {
++                        pid_t child;
++                        int fd[1];
++
++                        argv[i] = NULL;
++                        child = create_pipe_in ("git", "git", argv, NULL, NULL,
++                                                DEV_NULL, true, true, false, fd);
++                        if (child == -1)
++                          break;
++
++                        /* Read the subprocess output.  It is expected to be of the form
++                             T1 <space> <currdir_relative_filename1> NUL
++                             T2 <space> <currdir_relative_filename2> NUL
++                             ...
++                           where the relative filenames correspond to the given file
++                           names (because we have already relativized them).  */
++                        FILE *fp = fdopen (fd[0], "r");
++                        if (fp == NULL)
++                          error (EXIT_FAILURE, errno, _("fdopen() failed"));
++
++                        char *fn = NULL;
++                        size_t fn_size = 0;
++                        for (;;)
++                          {
++                            int status = fgetc (fp);
++                            if (status == EOF)
++                              break;
++                            /* status is a status tag, as documented in
++                               "man git-ls-files".  */
++
++                            int space = fgetc (fp);
++                            if (space != ' ')
++                              {
++                                fprintf (stderr, "vc-mtime: git ls-files output not as expected\n");
++                                break;
++                              }
++
++                            if (getdelim (&fn, &fn_size, '\0', fp) == -1)
++                              {
++                                if (errno == ENOMEM)
++                                  xalloc_die ();
++                                fprintf (stderr, "vc-mtime: failed to read git ls-files output\n");
++                                break;
++                              }
++                            signed char *vc_controlled_p =
++                              (signed char *) gl_map_get (relative_filenames_ht, fn);
++                            if (vc_controlled_p == NULL)
++                              fprintf (stderr, "vc-mtime: git ls-files returned an unexpected file name: %s\n", fn);
++                            else
++                              *vc_controlled_p = (status == 'H' ? 1 : 0);
++                          }
++
++                        free (fn);
++                        fclose (fp);
++
++                        /* Remove zombie process from process list, and retrieve exit status.  */
++                        int exitstatus =
++                          wait_subprocess (child, "git", false, true, true, false, NULL);
++                        if (exitstatus != 0)
++                          fprintf (stderr, "vc-mtime: git ls-files failed with exit code %d\n", exitstatus);
++                      }
++                    n0 = n;
++                  }
++                while (n0 < nfiles);
++
++                gl_map_free (relative_filenames_ht);
++              }
++
++              {
++                /* Put the relative file names into a hash table.  This is needed
++                   because 'git diff' returns the files in a different order
++                   than the one we provide in the command.  */
++                gl_map_t relative_filenames_ht =
++                  gl_map_create_empty (GL_HASH_MAP,
++                                       hashkey_string_equals, hashkey_string_hash,
++                                       NULL, NULL);
++                for (size_t n = 0; n < nfiles; n++)
++                  if (vc_controlled[n] == 1)
++                    {
++                      /* No need to test for duplicates here.  We have already set
++                         vc_controlled[n] to -1 for duplicates, above.  */
++                      gl_map_put (relative_filenames_ht, checkout_relative_filenames[n], &vc_controlled[n]);
++                    }
++
++                /* Run "git diff --name-only --no-relative -z HEAD -- FILE1..." for
++                   as many files as possible, and inspect the output.  */
++                size_t n0 = 0;
++                do
++                  {
++                    size_t i = 0;
++                    argv[i++] = "git";
++                    argv[i++] = "diff";
++                    argv[i++] = "--name-only";
++                    argv[i++] = "--no-relative";
++                    argv[i++] = "-z";
++                    argv[i++] = "HEAD";
++                    argv[i++] = "--";
++                    size_t i0 = i;
++
++                    size_t n = n0;
++                    size_t cmd_len = 46;
++                    for (; n < nfiles; n++)
++                      {
++                        if (vc_controlled[n] == 1)
++                          {
++                            if (cmd_len + strlen (currdir_relative_filenames[n]) >= MAX_CMD_LEN
++                                && i > i0)
++                              break;
++                            argv[i++] = currdir_relative_filenames[n];
++                            cmd_len += 1 + strlen (currdir_relative_filenames[n]);
++                          }
++                        n++;
++                      }
++                    if (i > i0)
++                      {
++                        pid_t child;
++                        int fd[1];
++
++                        argv[i] = NULL;
++                        child = create_pipe_in ("git", "git", argv, NULL, NULL,
++                                                DEV_NULL, true, true, false, fd);
++                        if (child == -1)
++                          break;
++
++                        /* Read the subprocess output.  It is expected to be of the form
++                             <checkout_relative_filename1> NUL
++                             <checkout_relative_filename2> NUL
++                             ...
++                           where the relative filenames are relative to the git
++                           checkout dir, not to currdir!  */
++                        FILE *fp = fdopen (fd[0], "r");
++                        if (fp == NULL)
++                          error (EXIT_FAILURE, errno, _("fdopen() failed"));
++
++                        char *fn = NULL;
++                        size_t fn_size = 0;
++                        for (;;)
++                          {
++                            /* Test for EOF.  */
++                            int c = fgetc (fp);
++                            if (c == EOF)
++                              break;
++                            ungetc (c, fp);
++
++                            if (getdelim (&fn, &fn_size, '\0', fp) == -1)
++                              {
++                                if (errno == ENOMEM)
++                                  xalloc_die ();
++                                fprintf (stderr, "vc-mtime: failed to read git diff output\n");
++                                break;
++                              }
++                            signed char *vc_controlled_p =
++                              (signed char *) gl_map_get (relative_filenames_ht, fn);
++                            if (vc_controlled_p == NULL)
++                              fprintf (stderr, "vc-mtime: git diff returned an unexpected file name: %s\n", fn);
++                            else
++                              /* filenames[n] is under version control but is modified.
++                                 Treat it like a file not under version control.  */
++                              *vc_controlled_p = 0;
++                          }
++
++                        free (fn);
++                        fclose (fp);
++
++                        /* Remove zombie process from process list, and retrieve exit status.  */
++                        int exitstatus =
++                          wait_subprocess (child, "git", false, true, true, false, NULL);
++                        if (exitstatus != 0)
++                          fprintf (stderr, "vc-mtime: git diff failed with exit code %d\n", exitstatus);
++                      }
++                    n0 = n;
++                  }
++                while (n0 < nfiles);
++
++                gl_map_free (relative_filenames_ht);
++              }
++
++              {
++                /* Run "git log -1 --format=%ct -- FILE1...".  It prints the
++                   time of last modification (the 'CommitDate', not the
++                   'AuthorDate' which merely represents the time at which the
++                   author locally committed the first version of the change),
++                   as the number of seconds since the Epoch.  The '--' option
++                   is for the case that the specified file was removed.  */
++                size_t n0 = 0;
++                do
++                  {
++                    size_t i = 0;
++                    argv[i++] = "git";
++                    argv[i++] = "log";
++                    argv[i++] = "-1";
++                    argv[i++] = "--format=%ct";
++                    argv[i++] = "--";
++                    size_t i0 = i;
++
++                    size_t n = n0;
++                    size_t cmd_len = 27;
++                    for (; n < nfiles; n++)
++                      {
++                        if (vc_controlled[n] == 1)
++                          {
++                            if (cmd_len + strlen (currdir_relative_filenames[n]) >= MAX_CMD_LEN
++                                && i > i0)
++                              break;
++                            argv[i++] = currdir_relative_filenames[n];
++                            cmd_len += 1 + strlen (currdir_relative_filenames[n]);
++                          }
++                        n++;
++                      }
++                    if (i > i0)
++                      {
++                        pid_t child;
++                        int fd[1];
++
++                        argv[i] = NULL;
++                        child = create_pipe_in ("git", "git", argv, NULL, NULL,
++                                                DEV_NULL, true, true, false, fd);
++                        if (child == -1)
++                          break;
++
++                        /* Read the subprocess output.  It is expected to be a
++                           single line, containing a positive integer.  */
++                        FILE *fp = fdopen (fd[0], "r");
++                        if (fp == NULL)
++                          error (EXIT_FAILURE, errno, _("fdopen() failed"));
++
++                        char *line = NULL;
++                        size_t linesize = 0;
++                        size_t linelen = getline (&line, &linesize, fp);
++                        if (linelen == (size_t)(-1))
++                          {
++                            if (errno == ENOMEM)
++                              xalloc_die ();
++                            fprintf (stderr, "vc-mtime: failed to read git log output\n");
++                           git_log_fail1:
++                            free (line);
++                            fclose (fp);
++                            wait_subprocess (child, "git", true, false, true, false, NULL);
++                           git_log_fail2:
++                            free (argv);
++                            for (size_t k = nfiles; k > 0; )
++                              free (currdir_relative_filenames[--k]);
++                            free (currdir_relative_filenames);
++                            for (size_t k = nfiles; k > 0; )
++                              free (checkout_relative_filenames[--k]);
++                            free (checkout_relative_filenames);
++                            free (git_checkout_slash);
++                            for (size_t k = nfiles; k > 0; )
++                              free (canonical_filenames[--k]);
++                            free (canonical_filenames);
++                            free (currdir);
++                            free (git_checkout);
++                            free (vc_controlled);
++                            return -1;
++                          }
++                        if (linelen > 0 && line[linelen - 1] == '\n')
++                          line[linelen - 1] = '\0';
++
++                        char *endptr;
++                        unsigned long git_log_time;
++                        if (!(xstrtoul (line, &endptr, 10, &git_log_time, NULL) == LONGINT_OK
++                              && endptr == line + strlen (line)))
++                          {
++                            fprintf (stderr, "vc-mtime: git log output not as expected\n");
++                            goto git_log_fail1;
++                          }
++
++                        struct timespec mtime;
++                        mtime.tv_sec = git_log_time;
++                        mtime.tv_nsec = 0;
++                        accumulate (&accu, mtime);
++
++                        free (line);
++                        fclose (fp);
++
++                        /* Remove zombie process from process list, and retrieve exit status.  */
++                        int exitstatus =
++                          wait_subprocess (child, "git", false, true, true, false, NULL);
++                        if (exitstatus != 0)
++                          {
++                            fprintf (stderr, "vc-mtime: git log failed with exit code %d\n", exitstatus);
++                            goto git_log_fail2;
++                          }
++                      }
++                    n0 = n;
++                  }
++                while (n0 < nfiles);
++              }
++
++              free (argv);
++              for (size_t k = nfiles; k > 0; )
++                free (currdir_relative_filenames[--k]);
++              free (currdir_relative_filenames);
++              for (size_t k = nfiles; k > 0; )
++                free (checkout_relative_filenames[--k]);
++              free (checkout_relative_filenames);
++              free (git_checkout_slash);
++              for (size_t k = nfiles; k > 0; )
++                free (canonical_filenames[--k]);
++              free (canonical_filenames);
++            }
++          free (currdir);
++        }
++      free (git_checkout);
++    }
++
++  /* For the files that are not under version control, or that are modified
++     compared to HEAD, use the file's time stamp.  */
++  for (size_t n = 0; n < nfiles; n++)
++    if (vc_controlled[n] == 0)
++      {
++        struct stat statbuf;
++        if (stat (filenames[n], &statbuf) < 0)
++          {
++            free (vc_controlled);
++            return -1;
++          }
++
++        struct timespec mtime = get_stat_mtime (&statbuf);
++        accumulate (&accu, mtime);
++      }
++
++  free (vc_controlled);
++
++  /* Since nfiles > 0, we must have accumulated at least one mtime.  */
++  if (!accu.has_some_mtimes)
++    abort ();
++  *max_of_mtimes = accu.max_of_mtimes;
++  return 0;
++}
++
++/* ========================================================================== */
++
++#ifdef TEST
++
++#include <assert.h>
++#include <stdio.h>
++#include <time.h>
++
++/* Some unit tests for internal functions.  */
++
++static void
++test_ancestor_level (void)
++{
++  assert (ancestor_level ("/home/user/projects/gnulib", "/home/user/projects/gnulib") == 0);
++  assert (ancestor_level ("/", "/") == 0);
++
++  assert (ancestor_level ("/home/user/projects/gnulib", "/home/user/projects/gnulib/lib/crypto") == 2);
++  assert (ancestor_level ("/", "/home/user") == 2);
++
++  assert (ancestor_level ("/home/user/.local", "/home/user/projects/gnulib") == -1);
++  assert (ancestor_level ("/.local", "/home/user") == -1);
++  assert (ancestor_level ("/.local", "/") == -1);
++}
++
++static void
++test_relativize (void)
++{
++  assert (strcmp (relativize ("/home/user/projects/gnulib",
++                              0, "/home/user/projects/gnulib", "/home/user/projects/gnulib"),
++                  ".") == 0);
++  assert (strcmp (relativize ("/home/user/projects/gnulib/NEWS",
++                              0, "/home/user/projects/gnulib", "/home/user/projects/gnulib"),
++                  "NEWS") == 0);
++  assert (strcmp (relativize ("/home/user/projects/gnulib/doc/Makefile",
++                              0, "/home/user/projects/gnulib", "/home/user/projects/gnulib"),
++                  "doc/Makefile") == 0);
++
++  assert (strcmp (relativize ("/",
++                              0, "/", "/"),
++                  ".") == 0);
++  assert (strcmp (relativize ("/swapfile",
++                              0, "/", "/"),
++                  "swapfile") == 0);
++  assert (strcmp (relativize ("/etc/passwd",
++                              0, "/", "/"),
++                  "etc/passwd") == 0);
++
++  assert (strcmp (relativize ("/home/user/projects/gnulib",
++                              2, "/home/user/projects/gnulib", "/home/user/projects/gnulib/lib/crypto"),
++                  "../../") == 0);
++  assert (strcmp (relativize ("/home/user/projects/gnulib/lib",
++                              2, "/home/user/projects/gnulib", "/home/user/projects/gnulib/lib/crypto"),
++                  "../") == 0);
++  assert (strcmp (relativize ("/home/user/projects/gnulib/lib/crypto",
++                              2, "/home/user/projects/gnulib", "/home/user/projects/gnulib/lib/crypto"),
++                  ".") == 0);
++  assert (strcmp (relativize ("/home/user/projects/gnulib/lib/malloc",
++                              2, "/home/user/projects/gnulib", "/home/user/projects/gnulib/lib/crypto"),
++                  "../malloc") == 0);
++  assert (strcmp (relativize ("/home/user/projects/gnulib/lib/cr",
++                              2, "/home/user/projects/gnulib", "/home/user/projects/gnulib/lib/crypto"),
++                  "../cr") == 0);
++  assert (strcmp (relativize ("/home/user/projects/gnulib/lib/cryptography",
++                              2, "/home/user/projects/gnulib", "/home/user/projects/gnulib/lib/crypto"),
++                  "../cryptography") == 0);
++  assert (strcmp (relativize ("/home/user/projects/gnulib/doc",
++                              2, "/home/user/projects/gnulib", "/home/user/projects/gnulib/lib/crypto"),
++                  "../../doc") == 0);
++  assert (strcmp (relativize ("/home/user/projects/gnulib/doc/Makefile",
++                              2, "/home/user/projects/gnulib", "/home/user/projects/gnulib/lib/crypto"),
++                  "../../doc/Makefile") == 0);
++
++  assert (strcmp (relativize ("/",
++                              2, "/", "/home/user"),
++                  "../../") == 0);
++  assert (strcmp (relativize ("/home",
++                              2, "/", "/home/user"),
++                  "../") == 0);
++  assert (strcmp (relativize ("/home/user",
++                              2, "/", "/home/user"),
++                  ".") == 0);
++  assert (strcmp (relativize ("/home/root",
++                              2, "/", "/home/user"),
++                  "../root") == 0);
++  assert (strcmp (relativize ("/home/us",
++                              2, "/", "/home/user"),
++                  "../us") == 0);
++  assert (strcmp (relativize ("/home/users",
++                              2, "/", "/home/user"),
++                  "../users") == 0);
++  assert (strcmp (relativize ("/etc",
++                              2, "/", "/home/user"),
++                  "../../etc") == 0);
++  assert (strcmp (relativize ("/etc/passwd",
++                              2, "/", "/home/user"),
++                  "../../etc/passwd") == 0);
++}
++
++/* Usage: ./a.out FILE[...]
++ */
++int
++main (int argc, char *argv[])
++{
++  test_ancestor_level ();
++  test_relativize ();
++
++  if (argc == 1)
++    {
++      fprintf (stderr, "Usage: ./a.out FILE[...]\n");
++      return 1;
++    }
++  struct timespec mtime;
++  int ret = max_vc_mtime (&mtime, argc - 1, (const char **) argv + 1);
++  if (ret == 0)
++    {
++      time_t t = mtime.tv_sec;
++      struct tm *gmt = gmtime (&t);
++      printf ("mtime = %04d-%02d-%02d %02d:%02d:%02d UTC\n",
++              gmt->tm_year + 1900, gmt->tm_mon + 1, gmt->tm_mday,
++              gmt->tm_hour, gmt->tm_min, gmt->tm_sec);
++      return 0;
++    }
++  else
++    {
++      printf ("failed\n");
++      return 1;
++    }
++}
++
++/*
++ * Local Variables:
++ * compile-command: "gcc -ggdb -DTEST -Wall -I. -I.. vc-mtime.c libgnu.a"
++ * End:
++ */
++
++#endif
+--- a/lib/vc-mtime.h
++++ b/lib/vc-mtime.h
+@@ -90,6 +90,13 @@ extern "C" {
+    Upon failure, it returns -1.  */
+ extern int vc_mtime (struct timespec *mtime, const char *filename);
+ 
++/* Determines the maximum of the version-controlled modification times of
++   FILENAMES[0..NFILES-1], and returns 0.
++   Upon failure, it returns -1.
++   NFILES must be > 0.  */
++extern int max_vc_mtime (struct timespec *max_of_mtimes,
++                         size_t nfiles, const char * const *filenames);
++
+ #ifdef __cplusplus
+ }
+ #endif
+--- a/modules/vc-mtime
++++ b/modules/vc-mtime
+@@ -16,6 +16,17 @@ error
+ getline
+ xstrtol
+ stat-time
++filename
++xalloc
++xgetcwd
++canonicalize-lgpl
++xvasprintf
++str_startswith
++map
++xmap
++hash-map
++hashkey-string
++getdelim
+ gettext-h
+ gnulib-i18n
+ 
diff --git a/tools/gnulib/patches/798-vc-mtime-add-api.patch b/tools/gnulib/patches/798-vc-mtime-add-api.patch
new file mode 100644
index 0000000000..2cf7edab4e
--- /dev/null
+++ b/tools/gnulib/patches/798-vc-mtime-add-api.patch
@@ -0,0 +1,91 @@
+From f4c40c2d6aabef8e587176bbf5226c8bc6649574 Mon Sep 17 00:00:00 2001
+From: Bruno Haible <bruno at clisp.org>
+Date: Fri, 2 May 2025 02:43:23 +0200
+Subject: [PATCH] vc-mtime: Add API for more efficient use of git, part 2.
+
+* lib/vc-mtime.c (max_vc_mtime): Don't skip the odd-numbered arguments.
+---
+ ChangeLog      |  5 +++++
+ lib/vc-mtime.c | 57 +++++++++++++++++++++-----------------------------
+ 2 files changed, 29 insertions(+), 33 deletions(-)
+
+--- a/lib/vc-mtime.c
++++ b/lib/vc-mtime.c
+@@ -558,17 +558,14 @@ max_vc_mtime (struct timespec *max_of_mt
+                     size_t n = n0;
+                     size_t cmd_len = 25;
+                     for (; n < nfiles; n++)
+-                      {
+-                        if (vc_controlled[n] == 1)
+-                          {
+-                            if (cmd_len + strlen (currdir_relative_filenames[n]) >= MAX_CMD_LEN
+-                                && i > i0)
+-                              break;
+-                            argv[i++] = currdir_relative_filenames[n];
+-                            cmd_len += 1 + strlen (currdir_relative_filenames[n]);
+-                          }
+-                        n++;
+-                      }
++                      if (vc_controlled[n] == 1)
++                        {
++                          if (cmd_len + strlen (currdir_relative_filenames[n]) >= MAX_CMD_LEN
++                              && i > i0)
++                            break;
++                          argv[i++] = currdir_relative_filenames[n];
++                          cmd_len += 1 + strlen (currdir_relative_filenames[n]);
++                        }
+                     if (i > i0)
+                       {
+                         pid_t child;
+@@ -672,17 +669,14 @@ max_vc_mtime (struct timespec *max_of_mt
+                     size_t n = n0;
+                     size_t cmd_len = 46;
+                     for (; n < nfiles; n++)
+-                      {
+-                        if (vc_controlled[n] == 1)
+-                          {
+-                            if (cmd_len + strlen (currdir_relative_filenames[n]) >= MAX_CMD_LEN
+-                                && i > i0)
+-                              break;
+-                            argv[i++] = currdir_relative_filenames[n];
+-                            cmd_len += 1 + strlen (currdir_relative_filenames[n]);
+-                          }
+-                        n++;
+-                      }
++                      if (vc_controlled[n] == 1)
++                        {
++                          if (cmd_len + strlen (currdir_relative_filenames[n]) >= MAX_CMD_LEN
++                              && i > i0)
++                            break;
++                          argv[i++] = currdir_relative_filenames[n];
++                          cmd_len += 1 + strlen (currdir_relative_filenames[n]);
++                        }
+                     if (i > i0)
+                       {
+                         pid_t child;
+@@ -768,17 +762,14 @@ max_vc_mtime (struct timespec *max_of_mt
+                     size_t n = n0;
+                     size_t cmd_len = 27;
+                     for (; n < nfiles; n++)
+-                      {
+-                        if (vc_controlled[n] == 1)
+-                          {
+-                            if (cmd_len + strlen (currdir_relative_filenames[n]) >= MAX_CMD_LEN
+-                                && i > i0)
+-                              break;
+-                            argv[i++] = currdir_relative_filenames[n];
+-                            cmd_len += 1 + strlen (currdir_relative_filenames[n]);
+-                          }
+-                        n++;
+-                      }
++                      if (vc_controlled[n] == 1)
++                        {
++                          if (cmd_len + strlen (currdir_relative_filenames[n]) >= MAX_CMD_LEN
++                              && i > i0)
++                            break;
++                          argv[i++] = currdir_relative_filenames[n];
++                          cmd_len += 1 + strlen (currdir_relative_filenames[n]);
++                        }
+                     if (i > i0)
+                       {
+                         pid_t child;
diff --git a/tools/gnulib/patches/799-vc-mtime-old-git.patch b/tools/gnulib/patches/799-vc-mtime-old-git.patch
new file mode 100644
index 0000000000..4c2082504b
--- /dev/null
+++ b/tools/gnulib/patches/799-vc-mtime-old-git.patch
@@ -0,0 +1,125 @@
+From 47548a77525a0f4489c9c420ccc2159079365da8 Mon Sep 17 00:00:00 2001
+From: Bruno Haible <bruno at clisp.org>
+Date: Fri, 2 May 2025 12:09:40 +0200
+Subject: [PATCH] vc-mtime: Make it work with git versions < 2.28.
+
+* lib/vc-mtime.c (git_version): New variable.
+(is_git_present): Read the output of "git --version", and set
+git_version.
+(max_vc_mtime): Don't pass option --no-relative if the git version
+is < 2.28.
+---
+ ChangeLog      |  9 ++++++
+ lib/vc-mtime.c | 82 +++++++++++++++++++++++++++++++++++++++++++++-----
+ 2 files changed, 83 insertions(+), 8 deletions(-)
+
+--- a/lib/vc-mtime.c
++++ b/lib/vc-mtime.c
+@@ -53,7 +53,9 @@
+ 
+ /* ========================================================================== */
+ 
+-/* Determines whether git is present.  */
++static const char *git_version;
++
++/* Determines whether git is present, and sets git_version if so.  */
+ static bool
+ is_git_present (void)
+ {
+@@ -63,17 +65,67 @@ is_git_present (void)
+   if (!git_tested)
+     {
+       /* Test for presence of git:
+-         "git --version >/dev/null 2>/dev/null"  */
++         "git --version 2>/dev/null"  */
+       const char *argv[3];
+-      int exitstatus;
++      pid_t child;
++      int fd[1];
+ 
+       argv[0] = "git";
+       argv[1] = "--version";
+       argv[2] = NULL;
+-      exitstatus = execute ("git", "git", argv, NULL, NULL,
+-                            false, false, true, true,
+-                            true, false, NULL);
+-      git_present = (exitstatus == 0);
++      child = create_pipe_in ("git", "git", argv, NULL, NULL,
++                              DEV_NULL, true, true, false, fd);
++      if (child == -1)
++        git_present = false;
++      else
++        {
++          /* Retrieve its result.  */
++          FILE *fp = fdopen (fd[0], "r");
++          if (fp == NULL)
++            error (EXIT_FAILURE, errno, _("fdopen() failed"));
++
++          char *line = NULL;
++          size_t linesize = 0;
++          size_t linelen = getline (&line, &linesize, fp);
++          if (linelen == (size_t)(-1))
++            {
++              fclose (fp);
++              wait_subprocess (child, "git", true, true, true, false, NULL);
++              git_present = false;
++            }
++          else
++            {
++              if (linelen > 0 && line[linelen - 1] == '\n')
++                line[linelen - 1] = '\0';
++
++              /* Read until EOF (otherwise the child process may get a SIGPIPE
++                 signal).  */
++              while (getc (fp) != EOF)
++                ;
++
++              fclose (fp);
++
++              /* Remove zombie process from process list, and retrieve exit
++                 status.  */
++              int exitstatus =
++                wait_subprocess (child, "git", true, true, true, false, NULL);
++              if (exitstatus != 0)
++                {
++                  free (line);
++                  git_present = false;
++                }
++              else
++                {
++                  /* The version starts at the first digit in the line.  */
++                  const char *p = line;
++                  for (; *p != '0'; p++)
++                    if (*p >= '0' && *p <= '9')
++                      break;
++                  git_version = p;
++                  git_present = true;
++                }
++            }
++        }
+       git_tested = true;
+     }
+ 
+@@ -660,7 +712,21 @@ max_vc_mtime (struct timespec *max_of_mt
+                     argv[i++] = "git";
+                     argv[i++] = "diff";
+                     argv[i++] = "--name-only";
+-                    argv[i++] = "--no-relative";
++                    /* With git versions >= 2.28, we pass option --no-relative,
++                       in order to neutralize any possible customization of the
++                       "diff.relative" property.  With git versions < 2.28, this
++                       is not needed, and the option --no-relative does not
++                       exist.  */
++                    if (!(git_version[0] <= '1'
++                          || (git_version[0] == '2' && git_version[1] == '.'
++                              && ((git_version[2] >= '0' && git_version[2] <= '9'
++                                   && !(git_version[3] >= '0' && git_version[3] <= '9'))
++                                  || (((git_version[2] == '1'
++                                        && git_version[3] >= '0' && git_version[3] <= '9')
++                                       || (git_version[2] == '2'
++                                           && git_version[3] >= '0' && git_version[3] <= '7'))
++                                      && !(git_version[4] >= '0' && git_version[4] <= '9'))))))
++                      argv[i++] = "--no-relative";
+                     argv[i++] = "-z";
+                     argv[i++] = "HEAD";
+                     argv[i++] = "--";
diff --git a/tools/gnulib/patches/900-str_startswith-module.patch b/tools/gnulib/patches/900-str_startswith-module.patch
new file mode 100644
index 0000000000..c9af62710a
--- /dev/null
+++ b/tools/gnulib/patches/900-str_startswith-module.patch
@@ -0,0 +1,117 @@
+From 24010120fab36721caaf92be076655571e44da07 Mon Sep 17 00:00:00 2001
+From: Bruno Haible <bruno at clisp.org>
+Date: Fri, 3 Jan 2025 09:26:14 +0100
+Subject: [PATCH] str_startswith: New module.
+
+* lib/string.in.h (str_startswith): New declaration.
+* lib/str_startswith.c: New file.
+* m4/string_h.m4 (gl_STRING_H_REQUIRE_DEFAULTS): Initialize
+GNULIB_STR_STARTSWITH.
+* modules/string-h (Makefile.am): Substitute GNULIB_STR_STARTSWITH.
+* modules/str_startswith: New file.
+---
+ ChangeLog              | 10 ++++++++++
+ lib/str_startswith.c   | 29 +++++++++++++++++++++++++++++
+ lib/string.in.h        |  8 ++++++++
+ m4/string_h.m4         |  3 ++-
+ modules/str_startswith | 23 +++++++++++++++++++++++
+ modules/string-h       |  1 +
+ 6 files changed, 73 insertions(+), 1 deletion(-)
+ create mode 100644 lib/str_startswith.c
+ create mode 100644 modules/str_startswith
+
+--- /dev/null
++++ b/lib/str_startswith.c
+@@ -0,0 +1,29 @@
++/* str_startswith function.
++   Copyright (C) 2025 Free Software Foundation, Inc.
++
++   This file is free software: you can redistribute it and/or modify
++   it under the terms of the GNU Lesser General Public License as
++   published by the Free Software Foundation, either version 3 of the
++   License, or (at your option) any later version.
++
++   This file is distributed in the hope that it will be useful,
++   but WITHOUT ANY WARRANTY; without even the implied warranty of
++   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
++   GNU Lesser General Public License for more details.
++
++   You should have received a copy of the GNU Lesser General Public License
++   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
++
++/* Written by Bruno Haible <bruno at clisp.org>, 2025.  */
++
++#include "config.h"
++
++/* Specification.  */
++#include <string.h>
++
++
++int
++str_startswith (const char *string, const char *prefix)
++{
++  return strncmp (string, prefix, strlen (prefix)) == 0;
++}
+--- a/lib/string.in.h
++++ b/lib/string.in.h
+@@ -1079,6 +1079,14 @@ _GL_WARN_ON_USE (strtok_r, "strtok_r is
+ /* The following functions are not specified by POSIX.  They are gnulib
+    extensions.  */
+ 
++#if @GNULIB_STR_STARTSWITH@
++/* Returns true if STRING starts with PREFIX.
++   Returns false otherwise.  */
++_GL_EXTERN_C int str_startswith (const char *string, const char *prefix)
++     _GL_ATTRIBUTE_PURE
++     _GL_ARG_NONNULL ((1, 2));
++#endif
++
+ #if @GNULIB_MBSLEN@
+ /* Return the number of multibyte characters in the character string STRING.
+    This considers multibyte characters, unlike strlen, which counts bytes.  */
+--- a/m4/string_h.m4
++++ b/m4/string_h.m4
+@@ -70,6 +70,7 @@ AC_DEFUN([gl_STRING_H_REQUIRE_DEFAULTS],
+     gl_MODULE_INDICATOR_INIT_VARIABLE([GNULIB_STRSTR])
+     gl_MODULE_INDICATOR_INIT_VARIABLE([GNULIB_STRCASESTR])
+     gl_MODULE_INDICATOR_INIT_VARIABLE([GNULIB_STRTOK_R])
++    gl_MODULE_INDICATOR_INIT_VARIABLE([GNULIB_STR_STARTSWITH])
+     gl_MODULE_INDICATOR_INIT_VARIABLE([GNULIB_MBSLEN])
+     gl_MODULE_INDICATOR_INIT_VARIABLE([GNULIB_MBSNLEN])
+     gl_MODULE_INDICATOR_INIT_VARIABLE([GNULIB_MBSCHR])
+--- /dev/null
++++ b/modules/str_startswith
+@@ -0,0 +1,23 @@
++Description:
++str_startswith() function: test whether a string starts with a given prefix.
++
++Files:
++lib/str_startswith.c
++
++Depends-on:
++string-h
++
++configure.ac:
++gl_STRING_MODULE_INDICATOR([str_startswith])
++
++Makefile.am:
++lib_SOURCES += str_startswith.c
++
++Include:
++<string.h>
++
++License:
++LGPLv2+
++
++Maintainer:
++all
+--- a/modules/string-h
++++ b/modules/string-h
+@@ -69,6 +69,7 @@ string.h: string.in.h $(top_builddir)/co
+ 	      -e 's/@''GNULIB_STRSTR''@/$(GNULIB_STRSTR)/g' \
+ 	      -e 's/@''GNULIB_STRCASESTR''@/$(GNULIB_STRCASESTR)/g' \
+ 	      -e 's/@''GNULIB_STRTOK_R''@/$(GNULIB_STRTOK_R)/g' \
++	      -e 's/@''GNULIB_STR_STARTSWITH''@/$(GNULIB_STR_STARTSWITH)/g' \
+ 	      -e 's/@''GNULIB_STRERROR''@/$(GNULIB_STRERROR)/g' \
+ 	      -e 's/@''GNULIB_STRERROR_R''@/$(GNULIB_STRERROR_R)/g' \
+ 	      -e 's/@''GNULIB_STRERRORNAME_NP''@/$(GNULIB_STRERRORNAME_NP)/g' \
diff --git a/tools/gnulib/patches/901-str_endswith-module.patch b/tools/gnulib/patches/901-str_endswith-module.patch
new file mode 100644
index 0000000000..00db5cdce0
--- /dev/null
+++ b/tools/gnulib/patches/901-str_endswith-module.patch
@@ -0,0 +1,119 @@
+From d89ac9373d9748f7601babf52c9129fcbcf0c907 Mon Sep 17 00:00:00 2001
+From: Bruno Haible <bruno at clisp.org>
+Date: Fri, 3 Jan 2025 09:54:14 +0100
+Subject: [PATCH] str_endswith: New module.
+
+* lib/string.in.h (str_endswith): New declaration.
+* lib/str_endswith.c: New file.
+* m4/string_h.m4 (gl_STRING_H_REQUIRE_DEFAULTS): Initialize
+GNULIB_STR_ENDSWITH.
+* modules/string-h (Makefile.am): Substitute GNULIB_STR_ENDSWITH.
+* modules/str_endswith: New file.
+---
+ ChangeLog            | 10 ++++++++++
+ lib/str_endswith.c   | 31 +++++++++++++++++++++++++++++++
+ lib/string.in.h      |  8 ++++++++
+ m4/string_h.m4       |  3 ++-
+ modules/str_endswith | 23 +++++++++++++++++++++++
+ modules/string-h     |  1 +
+ 6 files changed, 75 insertions(+), 1 deletion(-)
+ create mode 100644 lib/str_endswith.c
+ create mode 100644 modules/str_endswith
+
+--- /dev/null
++++ b/lib/str_endswith.c
+@@ -0,0 +1,31 @@
++/* str_endswith function.
++   Copyright (C) 2025 Free Software Foundation, Inc.
++
++   This file is free software: you can redistribute it and/or modify
++   it under the terms of the GNU Lesser General Public License as
++   published by the Free Software Foundation, either version 3 of the
++   License, or (at your option) any later version.
++
++   This file is distributed in the hope that it will be useful,
++   but WITHOUT ANY WARRANTY; without even the implied warranty of
++   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
++   GNU Lesser General Public License for more details.
++
++   You should have received a copy of the GNU Lesser General Public License
++   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
++
++/* Written by Bruno Haible <bruno at clisp.org>, 2025.  */
++
++#include "config.h"
++
++/* Specification.  */
++#include <string.h>
++
++
++int
++str_endswith (const char *string, const char *suffix)
++{
++  size_t len = strlen (string);
++  size_t n = strlen (suffix);
++  return len >= n && strcmp (string + len - n, suffix) == 0;
++}
+--- a/lib/string.in.h
++++ b/lib/string.in.h
+@@ -1087,6 +1087,14 @@ _GL_EXTERN_C int str_startswith (const c
+      _GL_ARG_NONNULL ((1, 2));
+ #endif
+ 
++#if @GNULIB_STR_ENDSWITH@
++/* Returns true if STRING ends with SUFFIX.
++   Returns false otherwise.  */
++_GL_EXTERN_C int str_endswith (const char *string, const char *prefix)
++     _GL_ATTRIBUTE_PURE
++     _GL_ARG_NONNULL ((1, 2));
++#endif
++
+ #if @GNULIB_MBSLEN@
+ /* Return the number of multibyte characters in the character string STRING.
+    This considers multibyte characters, unlike strlen, which counts bytes.  */
+--- a/m4/string_h.m4
++++ b/m4/string_h.m4
+@@ -71,6 +71,7 @@ AC_DEFUN([gl_STRING_H_REQUIRE_DEFAULTS],
+     gl_MODULE_INDICATOR_INIT_VARIABLE([GNULIB_STRCASESTR])
+     gl_MODULE_INDICATOR_INIT_VARIABLE([GNULIB_STRTOK_R])
+     gl_MODULE_INDICATOR_INIT_VARIABLE([GNULIB_STR_STARTSWITH])
++    gl_MODULE_INDICATOR_INIT_VARIABLE([GNULIB_STR_ENDSWITH])
+     gl_MODULE_INDICATOR_INIT_VARIABLE([GNULIB_MBSLEN])
+     gl_MODULE_INDICATOR_INIT_VARIABLE([GNULIB_MBSNLEN])
+     gl_MODULE_INDICATOR_INIT_VARIABLE([GNULIB_MBSCHR])
+--- /dev/null
++++ b/modules/str_endswith
+@@ -0,0 +1,23 @@
++Description:
++str_endswith() function: test whether a string ends with a given suffix.
++
++Files:
++lib/str_endswith.c
++
++Depends-on:
++string-h
++
++configure.ac:
++gl_STRING_MODULE_INDICATOR([str_endswith])
++
++Makefile.am:
++lib_SOURCES += str_endswith.c
++
++Include:
++<string.h>
++
++License:
++LGPLv2+
++
++Maintainer:
++all
+--- a/modules/string-h
++++ b/modules/string-h
+@@ -69,6 +69,7 @@ string.h: string.in.h $(top_builddir)/co
+ 	      -e 's/@''GNULIB_STRSTR''@/$(GNULIB_STRSTR)/g' \
+ 	      -e 's/@''GNULIB_STRCASESTR''@/$(GNULIB_STRCASESTR)/g' \
+ 	      -e 's/@''GNULIB_STRTOK_R''@/$(GNULIB_STRTOK_R)/g' \
++	      -e 's/@''GNULIB_STR_ENDSWITH''@/$(GNULIB_STR_ENDSWITH)/g' \
+ 	      -e 's/@''GNULIB_STR_STARTSWITH''@/$(GNULIB_STR_STARTSWITH)/g' \
+ 	      -e 's/@''GNULIB_STRERROR''@/$(GNULIB_STRERROR)/g' \
+ 	      -e 's/@''GNULIB_STRERROR_R''@/$(GNULIB_STRERROR_R)/g' \




More information about the lede-commits mailing list