diff --git a/compiler/GHC/Linker/Dynamic.hs b/compiler/GHC/Linker/Dynamic.hs
index 171503d4d66063860ac7cd9e7cfdddd55f97ce53..851789c4fae0b4f41ae6675ee18f46d397e0ccd0 100644
--- a/compiler/GHC/Linker/Dynamic.hs
+++ b/compiler/GHC/Linker/Dynamic.hs
@@ -11,6 +11,7 @@ where
 import GHC.Prelude
 import GHC.Platform
 import GHC.Platform.Ways
+import GHC.Settings (ToolSettings(toolSettings_ldSupportsSingleModule))
 
 import GHC.Driver.Config.Linker
 import GHC.Driver.Session
@@ -150,6 +151,9 @@ linkDynLib logger tmpfs dflags0 unit_env o_files dep_packages
             --   dynamic binding nonsense when referring to symbols from
             --   within the library. The NCG assumes that this option is
             --   specified (on i386, at least).
+            --   In XCode 15, -single_module is the default and passing the
+            --   flag is now obsolete and raises a warning (#24168). We encode
+            --   this information into the toolchain field ...SupportsSingleModule.
             -- -install_name
             --   Mac OS/X stores the path where a dynamic library is (to
             --   be) installed in the library itself.  It's called the
@@ -175,8 +179,11 @@ linkDynLib logger tmpfs dflags0 unit_env o_files dep_packages
                     ]
                  ++ map Option o_files
                  ++ [ Option "-undefined",
-                      Option "dynamic_lookup",
-                      Option "-single_module" ]
+                      Option "dynamic_lookup"
+                    ]
+                 ++ (if toolSettings_ldSupportsSingleModule (toolSettings dflags)
+                        then [ Option "-single_module" ]
+                        else [ ])
                  ++ (if platformArch platform `elem` [ ArchX86_64, ArchAArch64 ]
                      then [ ]
                      else [ Option "-Wl,-read_only_relocs,suppress" ])
diff --git a/compiler/GHC/Settings.hs b/compiler/GHC/Settings.hs
index 291c77b860bd1ba9591e727b2c860019f8694fae..8d750a15734c4a79524bd6f103e0fa2480c16f61 100644
--- a/compiler/GHC/Settings.hs
+++ b/compiler/GHC/Settings.hs
@@ -87,6 +87,7 @@ data Settings = Settings
 data ToolSettings = ToolSettings
   { toolSettings_ldSupportsCompactUnwind :: Bool
   , toolSettings_ldSupportsFilelist      :: Bool
+  , toolSettings_ldSupportsSingleModule  :: Bool
   , toolSettings_ldIsGnuLd               :: Bool
   , toolSettings_ccSupportsNoPie         :: Bool
   , toolSettings_useInplaceMinGW         :: Bool
diff --git a/compiler/GHC/Settings/IO.hs b/compiler/GHC/Settings/IO.hs
index 06952774fd610c546e1a943fcc151bee68190408..b6da658ffcba755e7140c50a90ece71fb58834d9 100644
--- a/compiler/GHC/Settings/IO.hs
+++ b/compiler/GHC/Settings/IO.hs
@@ -95,6 +95,7 @@ initSettings top_dir = do
       cxx_args = words cxx_args_str
   ldSupportsCompactUnwind <- getBooleanSetting "ld supports compact unwind"
   ldSupportsFilelist      <- getBooleanSetting "ld supports filelist"
+  ldSupportsSingleModule  <- getBooleanSetting "ld supports single module"
   ldIsGnuLd               <- getBooleanSetting "ld is GNU ld"
   arSupportsDashL         <- getBooleanSetting "ar supports -L"
 
@@ -163,6 +164,7 @@ initSettings top_dir = do
     , sToolSettings = ToolSettings
       { toolSettings_ldSupportsCompactUnwind = ldSupportsCompactUnwind
       , toolSettings_ldSupportsFilelist      = ldSupportsFilelist
+      , toolSettings_ldSupportsSingleModule  = ldSupportsSingleModule
       , toolSettings_ldIsGnuLd               = ldIsGnuLd
       , toolSettings_ccSupportsNoPie         = gccSupportsNoPie
       , toolSettings_useInplaceMinGW         = useInplaceMinGW
diff --git a/configure.ac b/configure.ac
index bbe7412b1549e91cf13ab6199dc491edd89f5638..ee5fa09451e637108cd33ed5011f4c4d0d49789f 100644
--- a/configure.ac
+++ b/configure.ac
@@ -502,6 +502,7 @@ FP_PROG_LD_IS_GNU
 FP_PROG_LD_BUILD_ID
 FP_PROG_LD_NO_COMPACT_UNWIND
 FP_PROG_LD_FILELIST
+FP_PROG_LD_SINGLE_MODULE
 
 dnl ** Which nm to use?
 dnl --------------------------------------------------------------
diff --git a/distrib/configure.ac.in b/distrib/configure.ac.in
index b3aa406ccf29c393800c0585833c992b55e27e0d..03ed77b8a4eb879562de57165630ab94ad79722e 100644
--- a/distrib/configure.ac.in
+++ b/distrib/configure.ac.in
@@ -134,6 +134,7 @@ FP_PROG_LD_IS_GNU
 FP_PROG_LD_BUILD_ID
 FP_PROG_LD_NO_COMPACT_UNWIND
 FP_PROG_LD_FILELIST
+FP_PROG_LD_SINGLE_MODULE
 
 dnl ** which strip to use?
 dnl --------------------------------------------------------------
diff --git a/hadrian/bindist/Makefile b/hadrian/bindist/Makefile
index 4c1a8cc130ee2970162233ac5f19238351813125..5e0eb8dde7a4a5edea932f23d83c704fea3622fa 100644
--- a/hadrian/bindist/Makefile
+++ b/hadrian/bindist/Makefile
@@ -92,6 +92,7 @@ lib/settings : config.mk
 	@echo ',("ld flags", "$(SettingsLdFlags)")' >> $@
 	@echo ',("ld supports compact unwind", "$(LdHasNoCompactUnwind)")' >> $@
 	@echo ',("ld supports filelist", "$(LdHasFilelist)")' >> $@
+	@echo ',("ld supports single module", "$(LdHasSingleModule)")' >> $@
 	@echo ',("ld is GNU ld", "$(LdIsGNULd)")' >> $@
 	@echo ',("Merge objects command", "$(SettingsMergeObjectsCommand)")' >> $@
 	@echo ',("Merge objects flags", "$(SettingsMergeObjectsFlags)")' >> $@
diff --git a/hadrian/bindist/config.mk.in b/hadrian/bindist/config.mk.in
index cad2ccc6e00259a85b01b4faf17edc7ed9e8492c..cb85c67a949be64c7aad86aff565bae5a722474f 100644
--- a/hadrian/bindist/config.mk.in
+++ b/hadrian/bindist/config.mk.in
@@ -240,6 +240,7 @@ LdHasBuildId = @LdHasBuildId@
 LdHasFilelist = @LdHasFilelist@
 LdIsGNULd = @LdIsGNULd@
 LdHasNoCompactUnwind = @LdHasNoCompactUnwind@
+LdHasSingleModule = @LdHasSingleModule@
 ArArgs = @ArArgs@
 ArSupportsAtFile = @ArSupportsAtFile@
 ArSupportsDashL  = @ArSupportsDashL@
diff --git a/hadrian/cfg/system.config.in b/hadrian/cfg/system.config.in
index 6a891b43632fd357c0f237977e2f950b0a5fdb5c..609ca46ea5fff9e656f2f5e894f17776da628177 100644
--- a/hadrian/cfg/system.config.in
+++ b/hadrian/cfg/system.config.in
@@ -141,6 +141,7 @@ gcc-extra-via-c-opts = @GccExtraViaCOpts@
 ld-has-no-compact-unwind = @LdHasNoCompactUnwind@
 ld-has-filelist = @LdHasFilelist@
 ld-is-gnu-ld = @LdIsGNULd@
+ld-supports-single-module = @LdHasSingleModule@
 ar-args = @ArArgs@
 
 settings-c-compiler-command = @SettingsCCompilerCommand@
diff --git a/hadrian/src/Rules/Generate.hs b/hadrian/src/Rules/Generate.hs
index 9efecaa57d887212fa7b5cda9a02727e61cd8149..a370635285b1efecc582505369f4b5ff320d628e 100644
--- a/hadrian/src/Rules/Generate.hs
+++ b/hadrian/src/Rules/Generate.hs
@@ -430,6 +430,7 @@ generateSettings = do
         , ("ld supports compact unwind", expr $ lookupSystemConfig "ld-has-no-compact-unwind")
         , ("ld supports filelist", expr $ lookupSystemConfig "ld-has-filelist")
         , ("ld is GNU ld", expr $ lookupSystemConfig "ld-is-gnu-ld")
+        , ("ld supports single module", expr $ lookupSystemConfig "ld-supports-single-module")
         , ("Merge objects command", expr $ settingsFileSetting SettingsFileSetting_MergeObjectsCommand)
         , ("Merge objects flags", expr $ settingsFileSetting SettingsFileSetting_MergeObjectsFlags)
         , ("ar command", expr $ settingsFileSetting SettingsFileSetting_ArCommand)
diff --git a/m4/fp_prog_ld_single_module.m4 b/m4/fp_prog_ld_single_module.m4
new file mode 100644
index 0000000000000000000000000000000000000000..3e124c81ca3eff3eda42074a53b82981e7b61db9
--- /dev/null
+++ b/m4/fp_prog_ld_single_module.m4
@@ -0,0 +1,30 @@
+# FP_PROG_LD_SINGLE_MODULE
+# ----------------------------
+# Sets the output variable LdHasSingleModule to YES if the darwin ld supports
+# -single_module, or NO otherwise.
+#
+# In XCode 15, -single_module is a default and passing it as a flag raises a
+# warning.
+AC_DEFUN([FP_PROG_LD_SINGLE_MODULE],
+[
+AC_CACHE_CHECK([whether ld supports -single_module], [fp_cv_ld_single_module],
+[
+case $target in
+  *-darwin)
+    echo 'int foo(int x) { return x*x; }' > conftest.c
+    echo 'extern int foo(int); int main() { return foo(5); }' > conftestmain.c
+    "$CC" -c -o conftestmain.o conftestmain.c
+    "$CC" -shared -o conftest.dylib conftest.c
+    if "$CC" -Wl,-single_module -o conftest conftestmain.o conftest.dylib 2>&1 | grep obsolete > /dev/null; then
+      fp_cv_ld_single_module=no
+    else
+      fp_cv_ld_single_module=yes
+    fi
+    rm -rf conftest* ;;
+  *)
+    fp_cv_ld_single_module=no ;;
+esac
+])
+FP_CAPITALIZE_YES_NO(["$fp_cv_ld_single_module"], [LdHasSingleModule])
+AC_SUBST([LdHasSingleModule])
+])# FP_PROG_LD_SINGLE_MODULE