This new module implements the "version-controlled modification time", as discussed in the thread "reproducible built files" [1][2] two months ago.
[1] https://lists.gnu.org/archive/html/bug-gnulib/2024-12/msg00182.html [2] https://lists.gnu.org/archive/html/bug-gnulib/2024-12/msg00197.html 2025-02-24 Bruno Haible <br...@clisp.org> vc-mtime: New module. * lib/vc-mtime.h: New file. * lib/vc-mtime.c: New file. * modules/vc-mtime: New file. =============================== lib/vc-mtime.h =============================== /* 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 <br...@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 */ =============================== lib/vc-mtime.c =============================== /* 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 <br...@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; } ============================== modules/vc-mtime ============================== 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