diff --git a/Makefile b/Makefile
index 3890f574eaa74116c1746f6b092907be3fb02642..12bae936b5e20f2bee96b924287a4bbb7bcabf3d 100644
--- a/Makefile
+++ b/Makefile
@@ -99,7 +99,7 @@ FIND_LIBHEADERS := find $(SRCINCDIR) -type f \( -name '*.h' -o \
 LIBHEADERS      := $(shell $(FIND_LIBHEADERS))
 ALLHEADERS      := $(LIBHEADERS) $(wildcard $(SRCDIR)/*.h)
 
-MULTISRCS       := $(SRCDIR)/fdopendir.c
+MULTISRCS       :=
 ADDSRCS         := $(SRCDIR)/add_symbols.c
 LIBSRCS         := $(filter-out $(MULTISRCS) $(ADDSRCS),$(wildcard $(SRCDIR)/*.c))
 
@@ -125,10 +125,16 @@ ALLSYSLIBOBJS   := $(ALLDLIBOBJS) $(ADDOBJS)
 # This not only reduces the size of the static library a bit, but also
 # avoids the "no symbols" warnings when creating it.
 #
+# A complication is that a completely empty static library is illegal,
+# so we provide a dummy object to be used when the library is logically
+# empty.
+#
 # This treatment is only applicable to the static library.
 EMPTY            = empty_source_content
 EMPTYSOBJ        = $(SRCDIR)/$(EMPTY)$(SLIBOBJEXT)
 SOBJLIST         = $(SRCDIR)/slibobjs.tmp
+DUMMYSRC         = $(SRCDIR)/dummylib.xxc
+DUMMYOBJ         = $(SRCDIR)/dummylib.o
 
 # Automatic tests that don't use the library, and are OK with -fno-builtin
 XTESTDIR          = xtest
@@ -337,9 +343,13 @@ slibobjs: $(ALLSLIBOBJS)
 allobjs: dlibobjs slibobjs syslibobjs
 
 # Create a list of nonempty static object files.
+# Since completely empty archives are illegal, we use our dummy if there
+# would otherwise be no objects.
 $(SOBJLIST): $(ALLSLIBOBJS)
 	$(CC) -c $(ALLCFLAGS) $(SLIBCFLAGS) -xc /dev/null -o $(EMPTYSOBJ)
+	$(CC) -c $(ALLCFLAGS) $(SLIBCFLAGS) -xc $(DUMMYSRC) -o $(DUMMYOBJ)
 	for f in $^; do cmp -s $(EMPTYSOBJ) $$f || echo $$f; done > $@
+	if [ ! -s $@ ]; then echo $(DUMMYOBJ) > $@; fi
 
 # Make the directories separate targets to avoid collisions in parallel builds.
 $(BUILDLIBDIR) $(DESTDIR)$(LIBDIR):
diff --git a/src/dummylib.xxc b/src/dummylib.xxc
new file mode 100644
index 0000000000000000000000000000000000000000..5172d75643d54dc46162c325f9bea6581424cdaa
--- /dev/null
+++ b/src/dummylib.xxc
@@ -0,0 +1,13 @@
+/* dummylib - mostly empty item containing one global definition. */
+/*
+ * When the legacy-support library becomes logically empty, it still needs
+ * to contain at least one object to keep the tools happy.  It also needs
+ * to define at least one global to avoid "no symbols" warnings.  This
+ * source is used to provide such an object.  Currently this happens on
+ * 10.15+.
+ *
+ * This source is actually C, but uses a different extension to defend
+ * against wildcarding in the Makefile.
+ */
+
+int __LEGACY_SUPPORT_LIBRARY_IS_INTENTIONALLY_LEFT_BLANK__ = 0;
diff --git a/src/fdopendir.c b/src/fdopendir.c
index 3af64b0a1ffdcd8d5918d35a69bbe85c6b666001..f11d9c52c1c228b9ed11ff620a8ef7c1a71b2b35 100644
--- a/src/fdopendir.c
+++ b/src/fdopendir.c
@@ -21,66 +21,209 @@
 /* Do our SDK-related setup */
 #include <_macports_extras/sdkversion.h>
 
+/*
+ * Implementation behavior largely follows these man page descriptions:
+ *
+ * https://www.freebsd.org/cgi/man.cgi?query=fdopendir&sektion=3
+ * https://linux.die.net/man/3/fdopendir
+ */
+
 #if __MPLS_LIB_SUPPORT_FDOPENDIR__
 
-#include "common-priv.h"
+/*
+ * Set up to use ino32 variants where possible.  This results in generating
+ * the "unadorned" names, which we augment with explicit suffixes where
+ * needed.  Similarly, we use the non-POSIX form in 32-bit builds.
+ *
+ * The ino32 variants are known to be unavailable in arm64 builds, but are
+ * available in all OS versions that need our fdopendir().
+ *
+ * Although the struct dirent is formatted differently for ino32 and ino64,
+ * we never directly reference it here.  The only difference in DIR is the
+ * format of the private struct _telldir pointed to by the __dd_td field,
+ * which we also never reference here.  Hence, we don't care which variant
+ * we're using, except for passing the choice through to the underlying
+ * functions.
+ *
+ * In the case of struct stat, we provide our own ino64 variant where needed.
+ */
+
+#define _DARWIN_NO_64_BIT_INODE 1
+#if !__MPLS_64BIT
+#define _NONSTD_SOURCE
+#endif
 
 #include <dirent.h>
+#include <stddef.h>
+
 #include <sys/stat.h>
 
+#include "common-priv.h"
+
 #if __MPLS_SDK_MAJOR < 1050
 #define __dd_fd dd_fd
-#endif /* __MPLS_SDK_MAJOR < 1050 */
+#endif /* __MPLS_SDK_MAJOR < 1050
+*/
+
+/* Define an ino64 struct stat if possible, else fall back to standard. */
+#ifdef __DARWIN_STRUCT_STAT64
+  struct local_stat64 __DARWIN_STRUCT_STAT64;
+  typedef struct local_stat64 local_stat64_t;
+#else
+  typedef struct stat local_stat64_t;
+#endif
+
+/* Universal stat buffer, accommodating both formats */
+union stat_u {
+  struct stat s;
+  local_stat64_t s64;
+};
+
+/* Type declarations for external functions */
+typedef int (stat_fn_t)(int fd, struct stat *buf);
+typedef int (stat64_fn_t)(int fd, local_stat64_t *buf);
+typedef DIR * (opn_fn_t)(const char *dirname);
+typedef void (rwd_fn_t)(DIR *dirp);
+
+/* Structure for per-variant dispatch table */
+typedef struct funcs_s {
+  stat_fn_t *do_fstat;
+  stat64_fn_t *do_fstat64;
+  opn_fn_t *do_open;
+  rwd_fn_t *do_rewind;
+} funcs_t;
+
+/* Common function used by all variants - controlled by a dispatch table */
+static DIR *
+fdopendir_internal(int fd, const funcs_t *funcs) {
+  DIR *dir;
+  int err;
+  mode_t mode;
+  union stat_u stbuf;
+
+  /* Do the appropriate fstat() on the supplied fd */
+  if (funcs->do_fstat64) {
+    err = (*funcs->do_fstat64)(fd, &stbuf.s64);
+    mode = stbuf.s64.st_mode;
+  } else if (funcs->do_fstat) {
+    err = (*funcs->do_fstat)(fd, &stbuf.s);
+    mode = stbuf.s.st_mode;
+  } else {
+    errno = EINVAL;  /* Should be impossible */
+    return NULL;
+  }
+
+  /* Fail if fd isn't a valid open fd */
+  if (err < 0) {
+    return NULL;
+  }
+
+  /* Fail if fd isn't a directory */
+  if (!S_ISDIR(mode)) {
+    errno = ENOTDIR;
+    return NULL;
+  }
+
+  /* Open given directory fd safely for iteration via readdir */
+
+  dir = _ATCALL(fd, ".", NULL, (*funcs->do_open)("."));
+  if (!dir) {
+    return NULL;
+  }
+
+  /*
+   * Replace underlying fd with supplied fd
+   * A subsequent closedir() will close fd
+   */
+
+  (void)close(dir->__dd_fd);
+  dir->__dd_fd = fd;
+
+  /*
+   * Rewind to the start of the directory, in case the underlying file
+   * is not positioned at the start
+   */
+
+  (*funcs->do_rewind)(dir);
+
+  /* Close given fd on exec (as per fdopendir() docs) */
+
+  (void)fcntl(fd, F_SETFD, FD_CLOEXEC);
+
+  return dir;
+}
 
 /*
- * Implementation behavior largely follows these man page descriptions:
+ * Now to handle all the variants:
  *
- * https://www.freebsd.org/cgi/man.cgi?query=fdopendir&sektion=3
- * https://linux.die.net/man/3/fdopendir
+ * Define a macro listing all variants supported by the OS/arch.
+ * 10.4 lacks the INODE64 variants.
+ * 64-bit builds lack the UNIX2003 variants.
  */
 
-DIR *fdopendir(int dirfd) {
-    DIR *dir;
-    struct stat dirstat;
+#if __MPLS_TARGET_OSVER < 1050
+#if !__MPLS_64BIT
 
-    /* Fail if dirfd isn't a valid open fd */
-    if (fstat(dirfd, &dirstat) < 0) {
-        return NULL;
-    }
+#define ALL_VARIANTS \
+  VARIANT_ENT(basic,,,) \
+  VARIANT_ENT(posix,,$UNIX2003,)
 
-    /* Fail if dirfd isn't a directory */
-    if (!S_ISDIR(dirstat.st_mode)) {
-        errno = ENOTDIR;
-        return NULL;
-    }
+#else /* __MPLS_64BIT */
 
-    /* Open given directory fd safely for iteration via readdir */
+#define ALL_VARIANTS \
+  VARIANT_ENT(basic,,,)
 
-    dir = _ATCALL(dirfd, ".", NULL, opendir("."));
-    if (!dir) {
-        return NULL;
-    }
+#endif /* __MPLS_64BIT */
+#else /* __MPLS_TARGET_OSVER >= 1050 */
+#if !__MPLS_64BIT
 
-    /*
-     * Replace underlying fd with supplied dirfd
-     * A subsequent closedir() will close dirfd
-     */
+#define ALL_VARIANTS \
+  VARIANT_ENT(basic,,,) \
+  VARIANT_ENT(posix,,$UNIX2003,) \
+  VARIANT_ENT(ino64,$INODE64,$UNIX2003,64)
 
-    (void)close(dir->__dd_fd);
-    dir->__dd_fd = dirfd;
+#else /* __MPLS_64BIT */
 
-    /*
-     * Rewind to the start of the directory, in case the underlying file
-     * is not positioned at the start
-     */
+#define ALL_VARIANTS \
+  VARIANT_ENT(basic,,,) \
+  VARIANT_ENT(ino64,$INODE64,,64)
 
-    rewinddir(dir);
+#endif /* __MPLS_64BIT */
+#endif /* __MPLS_TARGET_OSVER >= 1050 */
 
-    /* Close given fd on exec (as per fdopendir() docs) */
+/* Declare all called functions with appropriate suffixes. */
+/* The "basic" case is redundant but serves as an error check. */
+#define VARIANT_ENT(name,isfx,usfx,is64) \
+stat##is64##_fn_t fstat##isfx; \
+opn_fn_t opendir##isfx##usfx; \
+rwd_fn_t rewinddir##isfx##usfx;
+ALL_VARIANTS
+#undef VARIANT_ENT
 
-    (void)fcntl(dirfd, F_SETFD, FD_CLOEXEC);
-
-    return dir;
+/* Generate dispatch tables. */
+/*
+ * Since compilers aren't smart enough to avoid complaining about mismatched
+ * types from values made irrelevant by compile-time constants, we include
+ * technically incorrect casts to shut them up.
+ */
+#define VARIANT_ENT(name,isfx,usfx,is64) \
+static const funcs_t name = { \
+  .do_fstat = 0##is64 ? NULL : (stat_fn_t *) &fstat##isfx, \
+  .do_fstat64 = 0##is64 ? (stat64_fn_t *) &fstat##isfx : NULL, \
+  .do_open = &opendir##isfx##usfx, \
+  .do_rewind = &rewinddir##isfx##usfx, \
+};
+ALL_VARIANTS
+#undef VARIANT_ENT
+
+/* Now generate the functions. */
+#define VARIANT_ENT(name,isfx,usfx,is64) \
+DIR * \
+fdopendir##isfx##usfx(int fd) \
+{ \
+  return fdopendir_internal(fd, &name); \
 }
+ALL_VARIANTS
+#undef VARIANT_ENT
 
 #endif /* __MPLS_LIB_SUPPORT_FDOPENDIR__ */