diff --git a/src/backend/Makefile b/src/backend/Makefile
index 1aaf1ec2f5..25af514fba 100644
--- a/src/backend/Makefile
+++ b/src/backend/Makefile
@@ -53,7 +53,7 @@ endif
 
 ##########################################################################
 
-all: submake-libpgport submake-catalog-headers postgres $(POSTGRES_IMP)
+all: submake-libpgport submake-catalog-headers submake-utils-headers postgres $(POSTGRES_IMP)
 
 ifneq ($(PORTNAME), cygwin)
 ifneq ($(PORTNAME), win32)
@@ -136,24 +136,15 @@ parser/gram.h: parser/gram.y
 storage/lmgr/lwlocknames.h: storage/lmgr/generate-lwlocknames.pl storage/lmgr/lwlocknames.txt
 	$(MAKE) -C storage/lmgr lwlocknames.h lwlocknames.c
 
-utils/errcodes.h: utils/generate-errcodes.pl utils/errcodes.txt
-	$(MAKE) -C utils errcodes.h
-
-# see notes in src/backend/parser/Makefile
-utils/fmgrprotos.h: utils/fmgroids.h
-	touch $@
-
-utils/fmgroids.h: utils/Gen_fmgrtab.pl catalog/Catalog.pm $(top_srcdir)/src/include/catalog/pg_proc.dat $(top_srcdir)/src/include/access/transam.h
-	$(MAKE) -C utils fmgroids.h fmgrprotos.h
-
-utils/probes.h: utils/probes.d
-	$(MAKE) -C utils probes.h
-
 # run this unconditionally to avoid needing to know its dependencies here:
 submake-catalog-headers:
 	$(MAKE) -C catalog distprep generated-header-symlinks
 
-.PHONY: submake-catalog-headers
+# run this unconditionally to avoid needing to know its dependencies here:
+submake-utils-headers:
+	$(MAKE) -C utils distprep generated-header-symlinks
+
+.PHONY: submake-catalog-headers submake-utils-headers
 
 # Make symlinks for these headers in the include directory. That way
 # we can cut down on the -I options. Also, a symlink is automatically
@@ -168,7 +159,7 @@ submake-catalog-headers:
 
 .PHONY: generated-headers
 
-generated-headers: $(top_builddir)/src/include/parser/gram.h $(top_builddir)/src/include/storage/lwlocknames.h $(top_builddir)/src/include/utils/errcodes.h $(top_builddir)/src/include/utils/fmgroids.h $(top_builddir)/src/include/utils/fmgrprotos.h $(top_builddir)/src/include/utils/probes.h submake-catalog-headers
+generated-headers: $(top_builddir)/src/include/parser/gram.h $(top_builddir)/src/include/storage/lwlocknames.h submake-catalog-headers submake-utils-headers
 
 $(top_builddir)/src/include/parser/gram.h: parser/gram.h
 	prereqdir=`cd '$(dir $<)' >/dev/null && pwd` && \
@@ -180,25 +171,6 @@ $(top_builddir)/src/include/storage/lwlocknames.h: storage/lmgr/lwlocknames.h
 	  cd '$(dir $@)' && rm -f $(notdir $@) && \
 	  $(LN_S) "$$prereqdir/$(notdir $<)" .
 
-$(top_builddir)/src/include/utils/errcodes.h: utils/errcodes.h
-	prereqdir=`cd '$(dir $<)' >/dev/null && pwd` && \
-	  cd '$(dir $@)' && rm -f $(notdir $@) && \
-	  $(LN_S) "$$prereqdir/$(notdir $<)" .
-
-$(top_builddir)/src/include/utils/fmgroids.h: utils/fmgroids.h
-	prereqdir=`cd '$(dir $<)' >/dev/null && pwd` && \
-	  cd '$(dir $@)' && rm -f $(notdir $@) && \
-	  $(LN_S) "$$prereqdir/$(notdir $<)" .
-
-$(top_builddir)/src/include/utils/fmgrprotos.h: utils/fmgrprotos.h
-	prereqdir=`cd '$(dir $<)' >/dev/null && pwd` && \
-	  cd '$(dir $@)' && rm -f $(notdir $@) && \
-	  $(LN_S) "$$prereqdir/$(notdir $<)" .
-
-$(top_builddir)/src/include/utils/probes.h: utils/probes.h
-	cd '$(dir $@)' && rm -f $(notdir $@) && \
-	    $(LN_S) "../../../$(subdir)/utils/probes.h" .
-
 
 utils/probes.o: utils/probes.d $(SUBDIROBJS)
 	$(DTRACE) $(DTRACEFLAGS) -C -G -s $(call expand_subsys,$^) -o $@
@@ -213,7 +185,7 @@ distprep:
 	$(MAKE) -C catalog	distprep
 	$(MAKE) -C replication	repl_gram.c repl_scanner.c syncrep_gram.c syncrep_scanner.c
 	$(MAKE) -C storage/lmgr	lwlocknames.h lwlocknames.c
-	$(MAKE) -C utils	fmgrtab.c fmgroids.h fmgrprotos.h errcodes.h
+	$(MAKE) -C utils	distprep
 	$(MAKE) -C utils/misc	guc-file.c
 	$(MAKE) -C utils/sort	qsort_tuple.c
 
@@ -325,6 +297,7 @@ distclean: clean
 
 maintainer-clean: distclean
 	$(MAKE) -C catalog $@
+	$(MAKE) -C utils $@
 	rm -f bootstrap/bootparse.c \
 	      bootstrap/bootscanner.c \
 	      parser/gram.c \
@@ -336,10 +309,6 @@ maintainer-clean: distclean
 	      replication/syncrep_scanner.c \
 	      storage/lmgr/lwlocknames.c \
 	      storage/lmgr/lwlocknames.h \
-	      utils/fmgroids.h \
-	      utils/fmgrprotos.h \
-	      utils/fmgrtab.c \
-	      utils/errcodes.h \
 	      utils/misc/guc-file.c \
 	      utils/sort/qsort_tuple.c
 
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index a54197da51..0865240f11 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -79,8 +79,11 @@ distprep: bki-stamp
 
 generated-header-symlinks: $(top_builddir)/src/include/catalog/header-stamp
 
+# bki-stamp records the last time we ran genbki.pl.  We don't rely on
+# the timestamps of the individual output files, because the Perl script
+# won't update them if they didn't change (to avoid unnecessary recompiles).
 # Technically, this should depend on Makefile.global which supplies
-# $(MAJORVERSION); but then postgres.bki would need to be rebuilt after every
+# $(MAJORVERSION); but then genbki.pl would need to be re-run after every
 # configure run, even in distribution tarballs.  So depending on configure.in
 # instead is cheating a bit, but it will achieve the goal of updating the
 # version number when it changes.
diff --git a/src/backend/utils/.gitignore b/src/backend/utils/.gitignore
index f26215c631..0685556959 100644
--- a/src/backend/utils/.gitignore
+++ b/src/backend/utils/.gitignore
@@ -1,5 +1,6 @@
 /fmgrtab.c
 /fmgroids.h
 /fmgrprotos.h
+/fmgr-stamp
 /probes.h
 /errcodes.h
diff --git a/src/backend/utils/Makefile b/src/backend/utils/Makefile
index 343637af85..966e3bc2ed 100644
--- a/src/backend/utils/Makefile
+++ b/src/backend/utils/Makefile
@@ -21,23 +21,26 @@ catalogdir  = $(top_srcdir)/src/backend/catalog
 
 include $(top_srcdir)/src/backend/common.mk
 
-all: errcodes.h fmgroids.h fmgrprotos.h probes.h
+all: distprep probes.h generated-header-symlinks
 
-$(SUBDIRS:%=%-recursive): fmgroids.h fmgrprotos.h
+distprep: fmgr-stamp errcodes.h
+
+.PHONY: generated-header-symlinks
+
+generated-header-symlinks: $(top_builddir)/src/include/utils/header-stamp $(top_builddir)/src/include/utils/probes.h
+
+$(SUBDIRS:%=%-recursive): fmgr-stamp errcodes.h
 
 FMGR_DATA := $(addprefix $(top_srcdir)/src/include/catalog/,\
 	pg_language.dat pg_proc.dat \
 	)
 
-# see notes in src/backend/parser/Makefile
-fmgrprotos.h: fmgroids.h
-	touch $@
-
-fmgroids.h: fmgrtab.c
-	touch $@
-
-fmgrtab.c: Gen_fmgrtab.pl $(catalogdir)/Catalog.pm $(FMGR_DATA) $(top_srcdir)/src/include/access/transam.h
+# fmgr-stamp records the last time we ran Gen_fmgrtab.pl.  We don't rely on
+# the timestamps of the individual output files, because the Perl script
+# won't update them if they didn't change (to avoid unnecessary recompiles).
+fmgr-stamp: Gen_fmgrtab.pl $(catalogdir)/Catalog.pm $(FMGR_DATA) $(top_srcdir)/src/include/access/transam.h
 	$(PERL) -I $(catalogdir) $< -I $(top_srcdir)/src/include/ $(FMGR_DATA)
+	touch $@
 
 errcodes.h: $(top_srcdir)/src/backend/utils/errcodes.txt generate-errcodes.pl
 	$(PERL) $(srcdir)/generate-errcodes.pl $< > $@
@@ -55,6 +58,23 @@ else
 	sed -f $(srcdir)/Gen_dummy_probes.sed $< >$@
 endif
 
+# These generated headers must be symlinked into builddir/src/include/,
+# using absolute links for the reasons explained in src/backend/Makefile.
+# We use header-stamp to record that we've done this because the symlinks
+# themselves may appear older than fmgr-stamp.
+$(top_builddir)/src/include/utils/header-stamp: fmgr-stamp errcodes.h
+	prereqdir=`cd '$(dir $<)' >/dev/null && pwd` && \
+	cd '$(dir $@)' && for file in fmgroids.h fmgrprotos.h errcodes.h; do \
+	  rm -f $$file && $(LN_S) "$$prereqdir/$$file" . ; \
+	done
+	touch $@
+
+# probes.h is handled differently because it's not in the distribution tarball.
+$(top_builddir)/src/include/utils/probes.h: probes.h
+	cd '$(dir $@)' && rm -f $(notdir $@) && \
+	    $(LN_S) "../../../$(subdir)/probes.h" .
+
+
 .PHONY: install-data
 install-data: errcodes.txt installdirs
 	$(INSTALL_DATA) $(srcdir)/errcodes.txt '$(DESTDIR)$(datadir)/errcodes.txt'
@@ -66,10 +86,10 @@ installdirs:
 uninstall-data:
 	rm -f $(addprefix '$(DESTDIR)$(datadir)'/, errcodes.txt)
 
-# fmgroids.h, fmgrprotos.h, fmgrtab.c and errcodes.h are in the
+# fmgroids.h, fmgrprotos.h, fmgrtab.c, fmgr-stamp, and errcodes.h are in the
 # distribution tarball, so they are not cleaned here.
 clean:
 	rm -f probes.h
 
 maintainer-clean: clean
-	rm -f fmgroids.h fmgrprotos.h fmgrtab.c errcodes.h
+	rm -f fmgroids.h fmgrprotos.h fmgrtab.c fmgr-stamp errcodes.h
diff --git a/src/include/Makefile b/src/include/Makefile
index ba4b5f27fc..901eddbd44 100644
--- a/src/include/Makefile
+++ b/src/include/Makefile
@@ -77,7 +77,7 @@ uninstall:
 
 
 clean:
-	rm -f utils/fmgroids.h utils/fmgrprotos.h utils/errcodes.h
+	rm -f utils/fmgroids.h utils/fmgrprotos.h utils/errcodes.h utils/header-stamp
 	rm -f parser/gram.h storage/lwlocknames.h utils/probes.h
 	rm -f catalog/schemapg.h catalog/pg_*_d.h catalog/header-stamp
 
diff --git a/src/include/utils/.gitignore b/src/include/utils/.gitignore
index 25db658da5..05cfa7a8d6 100644
--- a/src/include/utils/.gitignore
+++ b/src/include/utils/.gitignore
@@ -2,3 +2,4 @@
 /fmgrprotos.h
 /probes.h
 /errcodes.h
+/header-stamp