From 34ee904517b3728567e4055c8d310fe6e7e2dcbb Mon Sep 17 00:00:00 2001
From: Yair Lenga <yair.lenga@gmail.com>
Date: Mon, 16 Feb 2026 12:57:50 -0500
Subject: [PATCH] Add support "valid-for" and "valid-max"

---
 src/remake.c      | 179 ++++++++++++++++++++++++++++++++++++++++++++++
 test-valid-for.mk |  58 +++++++++++++++
 2 files changed, 237 insertions(+)
 create mode 100644 test-valid-for.mk

diff --git a/src/remake.c b/src/remake.c
index adf39d46..850cf3eb 100644
--- a/src/remake.c
+++ b/src/remake.c
@@ -471,6 +471,165 @@ complain (struct file *file)
     }
 }
 
+/* Name of the target-specific variable that specifies a TTL
+   DURING which an existing target is considered up to date
+   without checking its prerequisites.
+
+   The value must be a concrete duration string consisting of
+   a positive integer optionally followed by a single unit
+   character: 's' (seconds), 'm' (minutes), 'h' (hours), or
+   'd' (days).  If no unit is given, seconds are assumed.
+
+   Leading and trailing whitespace is ignored.  Values
+   containing unexpanded '$' references are ignored.  */
+static const char valid_for_var[] = "valid-for";
+
+/* Name of the target-specific variable that specifies a TTL
+   AFTER which an existing target is considered out of date
+   and must be recreated, even if it is newer than its prerequisites. 
+   This is the opposite of valid-for, and is useful for targets that
+   have expensive dependencies or complex dependecies, which can
+   not be captured in a rule
+
+   The value is the same the 'valid_for'.
+*/
+
+static const char valid_max_var[] = "valid-max";
+
+/* Parse TTL strings like:  "30", "30s", "5m", "2h", "7d"
+   Returns 1 on success and stores seconds in *out.
+   Returns 0 on parse error or non-positive TTL. */
+static int
+parse_valid_time (const char *s, time_t *out)
+{
+  char *end;
+  const char *tail ;
+  long duration = 0 ;
+  long n;
+  long mult ;
+
+  if (s == 0)
+    return 0;
+
+  while (isspace ((unsigned char)*s))
+    ++s;
+
+  tail = s + strlen(s) ;
+  while (tail > s && isspace ((unsigned char)tail[-1]))
+    --tail;
+
+    /* Check for simple integer case (seconds) */
+  n = strtol (s, &end, 10);
+  if ( s != end && end == tail ) {
+    *out = (time_t) n;
+    return 1;
+  }
+
+  while ( s < tail ) {
+    if ( s == end )
+      return 0 ;
+    /* no value */
+
+    /* Check for a unit character.  */
+    switch (*end )
+    {
+      case 's': mult = 1; break;
+      case 'm': mult = 60; break;
+      case 'h': mult = 60 * 60; break;
+      case 'd': mult = 24 * 60 * 60; break;
+      case 'w': mult = 7 * 24 * 60 * 60; break;
+      default: return 0;
+    }    
+    duration += n * mult;
+    s = end + 1;
+    n = strtol (s, &end, 10);
+  }
+
+  *out = duration ;
+  return 1;
+}
+
+static const char *parse_valid_var(struct file *file, const char *var)
+{
+  struct variable *v;
+  const char *ttl_text;
+  v = lookup_variable_for_file (var, strlen (var), file);
+  if (!v || !v->value || !v->value[0])
+    return 0;
+
+  ttl_text = v->value;
+
+  while (isspace ((unsigned char)*ttl_text))
+    ++ttl_text;
+
+  if (*ttl_text == '\0')
+    return 0;
+
+  return ttl_text ;
+}
+
+
+/* Return 1 if FILE age is still within its "valid-for" TTL, and does not
+  need to be remade, regardless of the age of its prerequisites. */
+static int
+parse_valid_for_p (struct file *file, time_t age, unsigned int depth)
+{
+  const char *ttl_text;
+  time_t valid_until;
+
+  ttl_text = parse_valid_var(file, valid_for_var);
+
+  if ( ttl_text == 0 )
+    return 0;
+
+  if (!parse_valid_time (ttl_text, &valid_until)) {
+    DBS(DB_VERBOSE,
+       ("Ignoring %s value on target %s: '%s' (bad value)\n",
+        valid_for_var, file->name, ttl_text));
+    return 0;
+  }
+
+  if ( age < valid_until) {
+    DBS(DB_BASIC,
+       ("Marking %s: up to date (%s='%s', remaing ttl=%ld seconds)\n",
+        file->name, valid_for_var, ttl_text, (long) (valid_until - age)));
+    return 1;
+  }
+
+  return 0;
+}
+
+/* Return 1 if the file age is above valid-max, and has to be recreated regardless
+   of the age of its prerequisites.
+ */
+static int
+check_valid_max_p (struct file *file, long age, unsigned int depth)
+{
+  const char *ttl_text;
+  time_t expire_at;
+
+  ttl_text = parse_valid_var(file, valid_max_var);
+
+  if ( ttl_text == 0 )
+    return 0;
+
+  if (!parse_valid_time (ttl_text, &expire_at)) {
+    DBS(DB_VERBOSE,
+       ("Ignoring %s value on target %s: '%s' (bad value)\n",
+        valid_for_var, file->name, ttl_text));
+    return 0;
+  }
+  if (age > expire_at) {
+    DBS(DB_BASIC,
+       ("Marking %s out of date (%s=%s), expired=%ld seconds ago)\n",
+        file->name, valid_max_var, ttl_text, (long) (age - expire_at)));
+    return 1;
+  }
+
+  return 0;
+}
+
+
 /* Consider a single 'struct file' and update it as appropriate.
    Return an update_status value; use us_success if we aren't sure yet.  */
 
@@ -591,6 +750,26 @@ update_file_1 (struct file *file, unsigned int depth)
 
   must_make = noexist;
 
+  /* valid-for, valid-max TTL cache shortcut */
+  if (!noexist && !file->phony) {
+    int resolution ;
+    FILE_TIMESTAMP now = file_timestamp_now(&resolution) ;
+    long age = FILE_TIMESTAMP_S (now) - FILE_TIMESTAMP_S(this_mtime) ;
+
+    if ( parse_valid_for_p (file, age, depth))
+    {
+      file->updated = 1;
+      file->update_status = us_success;
+      notice_finished_file (file);
+      return us_success;
+    }
+    else if ( check_valid_max_p (file, age, depth))
+    {
+      must_make = 1 ;
+    }
+
+  }
+
   /* If file was specified as a target with no commands, come up with some
      default commands.  This may also add more also_make files.  */
 
diff --git a/test-valid-for.mk b/test-valid-for.mk
new file mode 100644
index 00000000..191f2671
--- /dev/null
+++ b/test-valid-for.mk
@@ -0,0 +1,58 @@
+.PHONY: all create clean debug
+
+MKFILE = $(lastword $(MAKEFILE_LIST))
+
+TESTS = valid-make.t valid-keep.t expire-make.t expire-keep.t t3-keep.t t5-keep.t t7-make.t
+
+all: create
+	$(MAKE) -r -f $(MKFILE) test
+	echo "Results:"
+	grep '' $(TESTS)
+
+debug: create
+	$(MAKE) -r -d -f $(MKFILE) test > $@.log
+	echo "Results:"
+	grep '' $(TESTS)
+	echo "Debug information in $@.log"
+
+clean:
+	rm -f -- $(TESTS)
+
+test: $(TESTS)
+
+create:
+	for f in $(TESTS) ; do echo keep > $$f ; done
+	touch -d "5 min ago" $(TESTS)
+	touch -d "3 min ago" t3-keep.t
+	touch -d "7 min ago" t7-make.t
+
+.PHONY: FORCE
+
+valid-make.t: private valid-for=4m
+valid-make.t: FORCE
+	echo "make" > $@
+
+valid-keep.t: private valid-for=6m
+valid-keep.t: FORCE
+	echo "make" > $@
+
+expire-make.t: private valid-max=4m
+expire-make.t:
+	echo "make" > $@
+
+expire-keep.t: private valid-max=6m
+expire-keep.t:
+	echo "make" > $@
+
+t3-keep.t t5-keep.t t7-make.t: private valid-for=4m
+t3-keep.t t5-keep.t t7-make.t: private valid-max=6m
+
+t3-keep.t:
+	echo "make" > $@
+
+t5-keep.t:
+	echo "make" > $@
+
+t7-make.t: 
+	echo "make" > $@
+
-- 
2.43.0

