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@pm.me>
Link: https://github.com/openwrt/openwrt/pull/16522
Signed-off-by: Robert Marko <robimarko@gmail.com>
This commit is contained in:
Michael Pratt 2025-05-31 02:47:11 -04:00 committed by Robert Marko
parent d19f8bc199
commit 1a253a2bb5
18 changed files with 4690 additions and 0 deletions

View File

@ -0,0 +1,494 @@
From 5a842672e79a7a5f6be837c483be4f9901a4ecc0 Mon Sep 17 00:00:00 2001
From: Bruno Haible <bruno@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@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

View File

@ -0,0 +1,218 @@
From 0b953ba82830f51ce9b939700705d238f9b0c0ba Mon Sep 17 00:00:00 2001
From: Bruno Haible <bruno@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

View File

@ -0,0 +1,294 @@
From 64042bb91aea5f854ca8a8938e2b3f7d1935e4f1 Mon Sep 17 00:00:00 2001
From: Bruno Haible <bruno@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

View File

@ -0,0 +1,215 @@
From 52738dcd0f522b16653cc8b21adfcb758702f2ab Mon Sep 17 00:00:00 2001
From: Bruno Haible <bruno@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

View File

@ -0,0 +1,176 @@
From e518788ad085e02b046e42889039a1f671e4619a Mon Sep 17 00:00:00 2001
From: Bruno Haible <bruno@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

View File

@ -0,0 +1,66 @@
From bb0f82be83d43db9cd77049be32ffd0b92ab5bb7 Mon Sep 17 00:00:00 2001
From: Bruno Haible <bruno@clisp.org>
Date: Fri, 24 Jan 2025 22:03:29 +0100
Subject: package-version: Simplify its use.
Reported by Basil L. Contovounesios <basil@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])
])
])

View File

@ -0,0 +1,89 @@
From 48648b4b9b3fd79a5c68913deb28678bd9d8eb34 Mon Sep 17 00:00:00 2001
From: Bruno Haible <bruno@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])

View File

@ -0,0 +1,32 @@
From 2e46209809f751087ca27523283bd5c3e9071d31 Mon Sep 17 00:00:00 2001
From: Bruno Haible <bruno@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.])
])

View File

@ -0,0 +1,75 @@
From 85599643e2fbf70f7f0bd58831993132ef335705 Mon Sep 17 00:00:00 2001
From: Bruno Haible <bruno@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

View File

@ -0,0 +1,366 @@
From 701d20aaf579bb71f35209dd63a272c3d9d21096 Mon Sep 17 00:00:00 2001
From: Bruno Haible <bruno@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@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@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

View File

@ -0,0 +1,64 @@
From f47c5f2e21d0ccedb271b406e35b6963b23a64c4 Mon Sep 17 00:00:00 2001
From: Bruno Haible <bruno@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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,44 @@
From 60cd34886c2c9f509974239fcf64a61f9a507d14 Mon Sep 17 00:00:00 2001
From: Bruno Haible <bruno@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]);

View File

@ -0,0 +1,968 @@
From 78269749030dde23182c29376d1410592436eb5d Mon Sep 17 00:00:00 2001
From: Bruno Haible <bruno@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

View File

@ -0,0 +1,91 @@
From f4c40c2d6aabef8e587176bbf5226c8bc6649574 Mon Sep 17 00:00:00 2001
From: Bruno Haible <bruno@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;

View File

@ -0,0 +1,125 @@
From 47548a77525a0f4489c9c420ccc2159079365da8 Mon Sep 17 00:00:00 2001
From: Bruno Haible <bruno@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++] = "--";

View File

@ -0,0 +1,117 @@
From 24010120fab36721caaf92be076655571e44da07 Mon Sep 17 00:00:00 2001
From: Bruno Haible <bruno@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@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' \

View File

@ -0,0 +1,119 @@
From d89ac9373d9748f7601babf52c9129fcbcf0c907 Mon Sep 17 00:00:00 2001
From: Bruno Haible <bruno@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@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' \