Merge topic 'optimize-static-library-deps'

2e42651dff Add option to optimize link dependencies for static libraries

Acked-by: Kitware Robot <kwrobot@kitware.com>
Merge-request: !5103
diff --git a/Help/manual/cmake-properties.7.rst b/Help/manual/cmake-properties.7.rst
index 30b2a05..afdf78c 100644
--- a/Help/manual/cmake-properties.7.rst
+++ b/Help/manual/cmake-properties.7.rst
@@ -307,6 +307,7 @@
    /prop_tgt/OBJCXX_EXTENSIONS
    /prop_tgt/OBJCXX_STANDARD
    /prop_tgt/OBJCXX_STANDARD_REQUIRED
+   /prop_tgt/OPTIMIZE_DEPENDENCIES
    /prop_tgt/OSX_ARCHITECTURES_CONFIG
    /prop_tgt/OSX_ARCHITECTURES
    /prop_tgt/OUTPUT_NAME_CONFIG
diff --git a/Help/manual/cmake-variables.7.rst b/Help/manual/cmake-variables.7.rst
index d780a65..4b40917 100644
--- a/Help/manual/cmake-variables.7.rst
+++ b/Help/manual/cmake-variables.7.rst
@@ -437,6 +437,7 @@
    /variable/CMAKE_NINJA_OUTPUT_PATH_PREFIX
    /variable/CMAKE_NO_BUILTIN_CHRPATH
    /variable/CMAKE_NO_SYSTEM_FROM_IMPORTED
+   /variable/CMAKE_OPTIMIZE_DEPENDENCIES
    /variable/CMAKE_OSX_ARCHITECTURES
    /variable/CMAKE_OSX_DEPLOYMENT_TARGET
    /variable/CMAKE_OSX_SYSROOT
diff --git a/Help/prop_tgt/OPTIMIZE_DEPENDENCIES.rst b/Help/prop_tgt/OPTIMIZE_DEPENDENCIES.rst
new file mode 100644
index 0000000..533cf6a
--- /dev/null
+++ b/Help/prop_tgt/OPTIMIZE_DEPENDENCIES.rst
@@ -0,0 +1,38 @@
+OPTIMIZE_DEPENDENCIES
+---------------------
+
+Activates dependency optimization of static and object libraries.
+
+When this property is set to true, some dependencies for a static or object
+library may be removed at generation time if they are not necessary to build
+the library, since static and object libraries don't actually link against
+anything.
+
+If a static or object library has dependency optimization enabled, it first
+discards all dependencies. Then, it looks through all of the direct and
+indirect dependencies that it initially had, and adds them back if they meet
+any of the following criteria:
+
+* The dependency was added to the library by :command:`add_dependencies`.
+* The dependency was added to the library through a source file in the library
+  generated by a custom command that uses the dependency.
+* The dependency has any ``PRE_BUILD``, ``PRE_LINK``, or ``POST_BUILD`` custom
+  commands associated with it.
+* The dependency contains any source files that were generated by a custom
+  command.
+* The dependency contains any languages which produce side effects that are
+  relevant to the library. Currently, all languages except C, C++, Objective-C,
+  Objective-C++, assembly, and CUDA are assumed to produce side effects.
+  However, side effects from one language are assumed not to be relevant to
+  another (for example, a Fortran library is assumed to not have any side
+  effects that are relevant for a Swift library.)
+
+As an example, assume you have a static Fortran library which depends on a
+static C library, which in turn depends on a static Fortran library. The
+top-level Fortran library has optimization enabled, but the middle C library
+does not. If you build the top Fortran library, the bottom Fortran library will
+also build, but not the middle C library, since the C library does not have any
+side effects that are relevant for the Fortran library. However, if you build
+the middle C library, the bottom Fortran library will also build, even though
+it does not have any side effects that are relevant to the C library, since the
+C library does not have optimization enabled.
diff --git a/Help/release/dev/optimize-link-dependencies.rst b/Help/release/dev/optimize-link-dependencies.rst
new file mode 100644
index 0000000..cfda826
--- /dev/null
+++ b/Help/release/dev/optimize-link-dependencies.rst
@@ -0,0 +1,7 @@
+optimize-link-dependencies
+--------------------------
+
+* A new target property, :prop_tgt:`OPTIMIZE_DEPENDENCIES`, was added to
+  avoid unnecessarily building dependencies for a static library.
+* A new variable, :variable:`CMAKE_OPTIMIZE_DEPENDENCIES`, was added to
+  initialize the :prop_tgt:`OPTIMIZE_DEPENDENCIES` target property.
diff --git a/Help/variable/CMAKE_OPTIMIZE_DEPENDENCIES.rst b/Help/variable/CMAKE_OPTIMIZE_DEPENDENCIES.rst
new file mode 100644
index 0000000..eed352a
--- /dev/null
+++ b/Help/variable/CMAKE_OPTIMIZE_DEPENDENCIES.rst
@@ -0,0 +1,4 @@
+CMAKE_OPTIMIZE_DEPENDENCIES
+---------------------------
+
+Initializes the :prop_tgt:`OPTIMIZE_DEPENDENCIES` target property.
diff --git a/Source/cmComputeTargetDepends.cxx b/Source/cmComputeTargetDepends.cxx
index e717f71..1f22ce6 100644
--- a/Source/cmComputeTargetDepends.cxx
+++ b/Source/cmComputeTargetDepends.cxx
@@ -17,10 +17,12 @@
 #include "cmMakefile.h"
 #include "cmMessageType.h"
 #include "cmPolicies.h"
+#include "cmProperty.h"
 #include "cmRange.h"
 #include "cmSourceFile.h"
 #include "cmState.h"
 #include "cmStateTypes.h"
+#include "cmStringAlgorithms.h"
 #include "cmSystemTools.h"
 #include "cmTarget.h"
 #include "cmTargetDepend.h"
@@ -115,19 +117,32 @@
   if (this->DebugMode) {
     this->DisplayGraph(this->InitialGraph, "initial");
   }
+  cmComputeComponentGraph ccg1(this->InitialGraph);
+  ccg1.Compute();
+  if (!this->CheckComponents(ccg1)) {
+    return false;
+  }
+
+  // Compute the intermediate graph.
+  this->CollectSideEffects();
+  this->ComputeIntermediateGraph();
+  if (this->DebugMode) {
+    this->DisplaySideEffects();
+    this->DisplayGraph(this->IntermediateGraph, "intermediate");
+  }
 
   // Identify components.
-  cmComputeComponentGraph ccg(this->InitialGraph);
-  ccg.Compute();
+  cmComputeComponentGraph ccg2(this->IntermediateGraph);
+  ccg2.Compute();
   if (this->DebugMode) {
-    this->DisplayComponents(ccg);
+    this->DisplayComponents(ccg2, "intermediate");
   }
-  if (!this->CheckComponents(ccg)) {
+  if (!this->CheckComponents(ccg2)) {
     return false;
   }
 
   // Compute the final dependency graph.
-  if (!this->ComputeFinalDepends(ccg)) {
+  if (!this->ComputeFinalDepends(ccg2)) {
     return false;
   }
   if (this->DebugMode) {
@@ -382,6 +397,111 @@
   }
 }
 
+void cmComputeTargetDepends::CollectSideEffects()
+{
+  this->SideEffects.resize(0);
+  this->SideEffects.resize(this->InitialGraph.size());
+
+  int n = static_cast<int>(this->InitialGraph.size());
+  std::set<int> visited;
+  for (int i = 0; i < n; ++i) {
+    this->CollectSideEffectsForTarget(visited, i);
+  }
+}
+
+void cmComputeTargetDepends::CollectSideEffectsForTarget(
+  std::set<int>& visited, int depender_index)
+{
+  if (!visited.count(depender_index)) {
+    auto& se = this->SideEffects[depender_index];
+    visited.insert(depender_index);
+    this->Targets[depender_index]->AppendCustomCommandSideEffects(
+      se.CustomCommandSideEffects);
+    this->Targets[depender_index]->AppendLanguageSideEffects(
+      se.LanguageSideEffects);
+
+    for (auto const& edge : this->InitialGraph[depender_index]) {
+      this->CollectSideEffectsForTarget(visited, edge);
+      auto const& dse = this->SideEffects[edge];
+      se.CustomCommandSideEffects.insert(dse.CustomCommandSideEffects.cbegin(),
+                                         dse.CustomCommandSideEffects.cend());
+      for (auto const& it : dse.LanguageSideEffects) {
+        se.LanguageSideEffects[it.first].insert(it.second.cbegin(),
+                                                it.second.cend());
+      }
+    }
+  }
+}
+
+void cmComputeTargetDepends::ComputeIntermediateGraph()
+{
+  this->IntermediateGraph.resize(0);
+  this->IntermediateGraph.resize(this->InitialGraph.size());
+
+  int n = static_cast<int>(this->InitialGraph.size());
+  for (int i = 0; i < n; ++i) {
+    auto const& initialEdges = this->InitialGraph[i];
+    auto& intermediateEdges = this->IntermediateGraph[i];
+    cmGeneratorTarget const* gt = this->Targets[i];
+    if (gt->GetType() != cmStateEnums::STATIC_LIBRARY &&
+        gt->GetType() != cmStateEnums::OBJECT_LIBRARY) {
+      intermediateEdges = initialEdges;
+    } else {
+      if (cmProp optimizeDependencies =
+            gt->GetProperty("OPTIMIZE_DEPENDENCIES")) {
+        if (cmIsOn(optimizeDependencies)) {
+          this->OptimizeLinkDependencies(gt, intermediateEdges, initialEdges);
+        } else {
+          intermediateEdges = initialEdges;
+        }
+      } else {
+        intermediateEdges = initialEdges;
+      }
+    }
+  }
+}
+
+void cmComputeTargetDepends::OptimizeLinkDependencies(
+  cmGeneratorTarget const* gt, cmGraphEdgeList& outputEdges,
+  cmGraphEdgeList const& inputEdges)
+{
+  std::set<int> emitted;
+  for (auto const& edge : inputEdges) {
+    if (edge.IsStrong()) {
+      // Preserve strong edges
+      outputEdges.push_back(edge);
+    } else {
+      auto const& dse = this->SideEffects[edge];
+
+      // Add edges that have custom command side effects
+      for (cmGeneratorTarget const* dep : dse.CustomCommandSideEffects) {
+        auto index = this->TargetIndex[dep];
+        if (!emitted.count(index)) {
+          emitted.insert(index);
+          outputEdges.push_back(
+            cmGraphEdge(index, false, edge.IsCross(), edge.GetBacktrace()));
+        }
+      }
+
+      // Add edges that have language side effects for languages we
+      // care about
+      for (auto const& lang : gt->GetAllConfigCompileLanguages()) {
+        auto it = dse.LanguageSideEffects.find(lang);
+        if (it != dse.LanguageSideEffects.end()) {
+          for (cmGeneratorTarget const* dep : it->second) {
+            auto index = this->TargetIndex[dep];
+            if (!emitted.count(index)) {
+              emitted.insert(index);
+              outputEdges.push_back(cmGraphEdge(index, false, edge.IsCross(),
+                                                edge.GetBacktrace()));
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
 void cmComputeTargetDepends::DisplayGraph(Graph const& graph,
                                           const std::string& name)
 {
@@ -402,10 +522,39 @@
   fprintf(stderr, "\n");
 }
 
-void cmComputeTargetDepends::DisplayComponents(
-  cmComputeComponentGraph const& ccg)
+void cmComputeTargetDepends::DisplaySideEffects()
 {
-  fprintf(stderr, "The strongly connected components are:\n");
+  fprintf(stderr, "The side effects are:\n");
+  int n = static_cast<int>(SideEffects.size());
+  for (int depender_index = 0; depender_index < n; ++depender_index) {
+    cmGeneratorTarget const* depender = this->Targets[depender_index];
+    fprintf(stderr, "target %d is [%s]\n", depender_index,
+            depender->GetName().c_str());
+    if (!this->SideEffects[depender_index].CustomCommandSideEffects.empty()) {
+      fprintf(stderr, "  custom commands\n");
+      for (auto const* gt :
+           this->SideEffects[depender_index].CustomCommandSideEffects) {
+        fprintf(stderr, "    from target %d [%s]\n", this->TargetIndex[gt],
+                gt->GetName().c_str());
+      }
+    }
+    for (auto const& it :
+         this->SideEffects[depender_index].LanguageSideEffects) {
+      fprintf(stderr, "  language %s\n", it.first.c_str());
+      for (auto const* gt : it.second) {
+        fprintf(stderr, "    from target %d [%s]\n", this->TargetIndex[gt],
+                gt->GetName().c_str());
+      }
+    }
+  }
+  fprintf(stderr, "\n");
+}
+
+void cmComputeTargetDepends::DisplayComponents(
+  cmComputeComponentGraph const& ccg, const std::string& name)
+{
+  fprintf(stderr, "The strongly connected components for the %s graph are:\n",
+          name.c_str());
   std::vector<NodeList> const& components = ccg.GetComponents();
   int n = static_cast<int>(components.size());
   for (int c = 0; c < n; ++c) {
diff --git a/Source/cmComputeTargetDepends.h b/Source/cmComputeTargetDepends.h
index e0d625f..277521d 100644
--- a/Source/cmComputeTargetDepends.h
+++ b/Source/cmComputeTargetDepends.h
@@ -42,6 +42,13 @@
                               cmTargetDependSet& deps);
 
 private:
+  struct TargetSideEffects
+  {
+    std::set<cmGeneratorTarget const*> CustomCommandSideEffects;
+    std::map<std::string, std::set<cmGeneratorTarget const*>>
+      LanguageSideEffects;
+  };
+
   void CollectTargets();
   void CollectDepends();
   void CollectTargetDepends(int depender_index);
@@ -50,6 +57,12 @@
   void AddTargetDepend(int depender_index, cmGeneratorTarget const* dependee,
                        cmListFileBacktrace const& dependee_backtrace,
                        bool linking, bool cross);
+  void CollectSideEffects();
+  void CollectSideEffectsForTarget(std::set<int>& visited, int depender_index);
+  void ComputeIntermediateGraph();
+  void OptimizeLinkDependencies(cmGeneratorTarget const* gt,
+                                cmGraphEdgeList& outputEdges,
+                                cmGraphEdgeList const& inputEdges);
   bool ComputeFinalDepends(cmComputeComponentGraph const& ccg);
   void AddInterfaceDepends(int depender_index, cmLinkItem const& dependee_name,
                            const std::string& config,
@@ -74,11 +87,15 @@
   using EdgeList = cmGraphEdgeList;
   using Graph = cmGraphAdjacencyList;
   Graph InitialGraph;
+  Graph IntermediateGraph;
   Graph FinalGraph;
+  std::vector<TargetSideEffects> SideEffects;
   void DisplayGraph(Graph const& graph, const std::string& name);
+  void DisplaySideEffects();
 
   // Deal with connected components.
-  void DisplayComponents(cmComputeComponentGraph const& ccg);
+  void DisplayComponents(cmComputeComponentGraph const& ccg,
+                         const std::string& name);
   bool CheckComponents(cmComputeComponentGraph const& ccg);
   void ComplainAboutBadComponent(cmComputeComponentGraph const& ccg, int c,
                                  bool strong = false);
diff --git a/Source/cmGeneratorTarget.cxx b/Source/cmGeneratorTarget.cxx
index 889ad7c..9611e14 100644
--- a/Source/cmGeneratorTarget.cxx
+++ b/Source/cmGeneratorTarget.cxx
@@ -1084,6 +1084,37 @@
   return this->Target->GetPostBuildCommands();
 }
 
+void cmGeneratorTarget::AppendCustomCommandSideEffects(
+  std::set<cmGeneratorTarget const*>& sideEffects) const
+{
+  if (!this->GetPreBuildCommands().empty() ||
+      !this->GetPreLinkCommands().empty() ||
+      !this->GetPostBuildCommands().empty()) {
+    sideEffects.insert(this);
+  } else {
+    for (auto const& source : this->GetAllConfigSources()) {
+      if (source.Source->GetCustomCommand() != nullptr) {
+        sideEffects.insert(this);
+        break;
+      }
+    }
+  }
+}
+
+void cmGeneratorTarget::AppendLanguageSideEffects(
+  std::map<std::string, std::set<cmGeneratorTarget const*>>& sideEffects) const
+{
+  static const std::set<cm::string_view> LANGS_WITH_NO_SIDE_EFFECTS = {
+    "C"_s, "CXX"_s, "OBJC"_s, "OBJCXX"_s, "ASM"_s, "CUDA"_s,
+  };
+
+  for (auto const& lang : this->GetAllConfigCompileLanguages()) {
+    if (!LANGS_WITH_NO_SIDE_EFFECTS.count(lang)) {
+      sideEffects[lang].insert(this);
+    }
+  }
+}
+
 bool cmGeneratorTarget::IsInBuildSystem() const
 {
   if (this->IsImported()) {
diff --git a/Source/cmGeneratorTarget.h b/Source/cmGeneratorTarget.h
index 4a03f65..69c5faf 100644
--- a/Source/cmGeneratorTarget.h
+++ b/Source/cmGeneratorTarget.h
@@ -55,6 +55,12 @@
   std::vector<cmCustomCommand> const& GetPreLinkCommands() const;
   std::vector<cmCustomCommand> const& GetPostBuildCommands() const;
 
+  void AppendCustomCommandSideEffects(
+    std::set<cmGeneratorTarget const*>& sideEffects) const;
+  void AppendLanguageSideEffects(
+    std::map<std::string, std::set<cmGeneratorTarget const*>>& sideEffects)
+    const;
+
 #define DECLARE_TARGET_POLICY(POLICY)                                         \
   cmPolicies::PolicyStatus GetPolicyStatus##POLICY() const                    \
   {                                                                           \
diff --git a/Source/cmTarget.cxx b/Source/cmTarget.cxx
index 51b4e9e..60416a3 100644
--- a/Source/cmTarget.cxx
+++ b/Source/cmTarget.cxx
@@ -373,6 +373,7 @@
     initProp("VS_JUST_MY_CODE_DEBUGGING");
     initProp("DISABLE_PRECOMPILE_HEADERS");
     initProp("UNITY_BUILD");
+    initProp("OPTIMIZE_DEPENDENCIES");
     initPropValue("UNITY_BUILD_BATCH_SIZE", "8");
     initPropValue("UNITY_BUILD_MODE", "BATCH");
     initPropValue("PCH_WARN_INVALID", "ON");
diff --git a/Tests/RunCMake/CMakeLists.txt b/Tests/RunCMake/CMakeLists.txt
index e08b30a..98a1f87 100644
--- a/Tests/RunCMake/CMakeLists.txt
+++ b/Tests/RunCMake/CMakeLists.txt
@@ -401,6 +401,7 @@
 add_RunCMake_test(configure_file)
 add_RunCMake_test(CTestTimeout -DTIMEOUT=${CTestTestTimeout_TIME})
 add_RunCMake_test(CTestTimeoutAfterMatch)
+add_RunCMake_test(DependencyGraph -DCMAKE_Fortran_COMPILER=${CMAKE_Fortran_COMPILER})
 
 # ctresalloc links against CMakeLib and CTestLib, which means it can't be built
 # if CMake_TEST_EXTERNAL_CMAKE is activated (the compiler might be different.)
diff --git a/Tests/RunCMake/DependencyGraph/CMakeLists.txt b/Tests/RunCMake/DependencyGraph/CMakeLists.txt
new file mode 100644
index 0000000..b646c4a
--- /dev/null
+++ b/Tests/RunCMake/DependencyGraph/CMakeLists.txt
@@ -0,0 +1,3 @@
+cmake_minimum_required(VERSION 3.18)
+project(${RunCMake_TEST} NONE)
+include(${RunCMake_TEST}.cmake)
diff --git a/Tests/RunCMake/DependencyGraph/OptimizeCommon.cmake b/Tests/RunCMake/DependencyGraph/OptimizeCommon.cmake
new file mode 100644
index 0000000..4954bc4
--- /dev/null
+++ b/Tests/RunCMake/DependencyGraph/OptimizeCommon.cmake
@@ -0,0 +1,40 @@
+enable_language(C)
+
+set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY out)
+set(CMAKE_LIBRARY_OUTPUT_DIRECTORY out)
+set(CMAKE_RUNTIME_OUTPUT_DIRECTORY out)
+
+add_library(SharedTop SHARED mylib.c)
+add_library(StaticTop STATIC mylib.c)
+add_library(StaticMiddle STATIC mylib.c)
+
+add_library(StaticNone STATIC mylib.c)
+add_library(StaticPreBuild STATIC mylib.c)
+add_library(StaticPreLink STATIC mylib.c)
+add_library(StaticPostBuild STATIC mylib.c)
+add_library(StaticCc STATIC mylibcc.c)
+
+add_custom_command(TARGET StaticPreBuild PRE_BUILD
+  COMMAND ${CMAKE_COMMAND} -E true)
+add_custom_command(TARGET StaticPreLink PRE_LINK
+  COMMAND ${CMAKE_COMMAND} -E true)
+add_custom_command(TARGET StaticPostBuild POST_BUILD
+  COMMAND ${CMAKE_COMMAND} -E true)
+add_custom_command(OUTPUT mylibcc.c
+  COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_SOURCE_DIR}/mylib.c ${CMAKE_BINARY_DIR}/mylibcc.c)
+
+target_link_libraries(SharedTop PRIVATE StaticMiddle)
+target_link_libraries(StaticTop PRIVATE StaticMiddle)
+target_link_libraries(StaticMiddle PRIVATE StaticNone StaticPreBuild StaticPreLink StaticPostBuild StaticCc)
+
+if(OPTIMIZE_TOP)
+  set_target_properties(SharedTop StaticTop PROPERTIES
+    OPTIMIZE_DEPENDENCIES TRUE)
+endif()
+if(OPTIMIZE_MIDDLE)
+  set_target_properties(StaticMiddle PROPERTIES
+    OPTIMIZE_DEPENDENCIES TRUE)
+endif()
+
+include(WriteTargets.cmake)
+write_targets()
diff --git a/Tests/RunCMake/DependencyGraph/OptimizeFortran-both-build-check.cmake b/Tests/RunCMake/DependencyGraph/OptimizeFortran-both-build-check.cmake
new file mode 100644
index 0000000..1020cb3
--- /dev/null
+++ b/Tests/RunCMake/DependencyGraph/OptimizeFortran-both-build-check.cmake
@@ -0,0 +1,5 @@
+include(${RunCMake_TEST_BINARY_DIR}/target_files.cmake)
+check_files(${RunCMake_TEST_BINARY_DIR}/out
+  ${FortranTop_TARGET_FILE}
+  ${FortranBottom_TARGET_FILE}
+  )
diff --git a/Tests/RunCMake/DependencyGraph/OptimizeFortran-both-build-stderr.txt b/Tests/RunCMake/DependencyGraph/OptimizeFortran-both-build-stderr.txt
new file mode 100644
index 0000000..8d98f9d
--- /dev/null
+++ b/Tests/RunCMake/DependencyGraph/OptimizeFortran-both-build-stderr.txt
@@ -0,0 +1 @@
+.*
diff --git a/Tests/RunCMake/DependencyGraph/OptimizeFortran-both.cmake b/Tests/RunCMake/DependencyGraph/OptimizeFortran-both.cmake
new file mode 100644
index 0000000..581fd46
--- /dev/null
+++ b/Tests/RunCMake/DependencyGraph/OptimizeFortran-both.cmake
@@ -0,0 +1 @@
+include(OptimizeFortranCommon.cmake)
diff --git a/Tests/RunCMake/DependencyGraph/OptimizeFortran-middle-build-check.cmake b/Tests/RunCMake/DependencyGraph/OptimizeFortran-middle-build-check.cmake
new file mode 100644
index 0000000..5c7e8cd
--- /dev/null
+++ b/Tests/RunCMake/DependencyGraph/OptimizeFortran-middle-build-check.cmake
@@ -0,0 +1,6 @@
+include(${RunCMake_TEST_BINARY_DIR}/target_files.cmake)
+check_files(${RunCMake_TEST_BINARY_DIR}/out
+  ${FortranTop_TARGET_FILE}
+  ${CMiddle_TARGET_FILE}
+  ${FortranBottom_TARGET_FILE}
+  )
diff --git a/Tests/RunCMake/DependencyGraph/OptimizeFortran-middle-build-stderr.txt b/Tests/RunCMake/DependencyGraph/OptimizeFortran-middle-build-stderr.txt
new file mode 100644
index 0000000..8d98f9d
--- /dev/null
+++ b/Tests/RunCMake/DependencyGraph/OptimizeFortran-middle-build-stderr.txt
@@ -0,0 +1 @@
+.*
diff --git a/Tests/RunCMake/DependencyGraph/OptimizeFortran-middle.cmake b/Tests/RunCMake/DependencyGraph/OptimizeFortran-middle.cmake
new file mode 100644
index 0000000..581fd46
--- /dev/null
+++ b/Tests/RunCMake/DependencyGraph/OptimizeFortran-middle.cmake
@@ -0,0 +1 @@
+include(OptimizeFortranCommon.cmake)
diff --git a/Tests/RunCMake/DependencyGraph/OptimizeFortran-none-build-check.cmake b/Tests/RunCMake/DependencyGraph/OptimizeFortran-none-build-check.cmake
new file mode 100644
index 0000000..5c7e8cd
--- /dev/null
+++ b/Tests/RunCMake/DependencyGraph/OptimizeFortran-none-build-check.cmake
@@ -0,0 +1,6 @@
+include(${RunCMake_TEST_BINARY_DIR}/target_files.cmake)
+check_files(${RunCMake_TEST_BINARY_DIR}/out
+  ${FortranTop_TARGET_FILE}
+  ${CMiddle_TARGET_FILE}
+  ${FortranBottom_TARGET_FILE}
+  )
diff --git a/Tests/RunCMake/DependencyGraph/OptimizeFortran-none-build-stderr.txt b/Tests/RunCMake/DependencyGraph/OptimizeFortran-none-build-stderr.txt
new file mode 100644
index 0000000..8d98f9d
--- /dev/null
+++ b/Tests/RunCMake/DependencyGraph/OptimizeFortran-none-build-stderr.txt
@@ -0,0 +1 @@
+.*
diff --git a/Tests/RunCMake/DependencyGraph/OptimizeFortran-none.cmake b/Tests/RunCMake/DependencyGraph/OptimizeFortran-none.cmake
new file mode 100644
index 0000000..581fd46
--- /dev/null
+++ b/Tests/RunCMake/DependencyGraph/OptimizeFortran-none.cmake
@@ -0,0 +1 @@
+include(OptimizeFortranCommon.cmake)
diff --git a/Tests/RunCMake/DependencyGraph/OptimizeFortran-top-build-check.cmake b/Tests/RunCMake/DependencyGraph/OptimizeFortran-top-build-check.cmake
new file mode 100644
index 0000000..1020cb3
--- /dev/null
+++ b/Tests/RunCMake/DependencyGraph/OptimizeFortran-top-build-check.cmake
@@ -0,0 +1,5 @@
+include(${RunCMake_TEST_BINARY_DIR}/target_files.cmake)
+check_files(${RunCMake_TEST_BINARY_DIR}/out
+  ${FortranTop_TARGET_FILE}
+  ${FortranBottom_TARGET_FILE}
+  )
diff --git a/Tests/RunCMake/DependencyGraph/OptimizeFortran-top-build-stderr.txt b/Tests/RunCMake/DependencyGraph/OptimizeFortran-top-build-stderr.txt
new file mode 100644
index 0000000..8d98f9d
--- /dev/null
+++ b/Tests/RunCMake/DependencyGraph/OptimizeFortran-top-build-stderr.txt
@@ -0,0 +1 @@
+.*
diff --git a/Tests/RunCMake/DependencyGraph/OptimizeFortran-top.cmake b/Tests/RunCMake/DependencyGraph/OptimizeFortran-top.cmake
new file mode 100644
index 0000000..581fd46
--- /dev/null
+++ b/Tests/RunCMake/DependencyGraph/OptimizeFortran-top.cmake
@@ -0,0 +1 @@
+include(OptimizeFortranCommon.cmake)
diff --git a/Tests/RunCMake/DependencyGraph/OptimizeFortranCommon.cmake b/Tests/RunCMake/DependencyGraph/OptimizeFortranCommon.cmake
new file mode 100644
index 0000000..354d3fc
--- /dev/null
+++ b/Tests/RunCMake/DependencyGraph/OptimizeFortranCommon.cmake
@@ -0,0 +1,25 @@
+enable_language(C)
+enable_language(Fortran)
+
+set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY out)
+set(CMAKE_LIBRARY_OUTPUT_DIRECTORY out)
+set(CMAKE_RUNTIME_OUTPUT_DIRECTORY out)
+
+add_library(FortranTop STATIC mylib.f90)
+add_library(CMiddle STATIC mylib.c)
+add_library(FortranBottom STATIC mylib.f90)
+
+target_link_libraries(FortranTop PRIVATE CMiddle)
+target_link_libraries(CMiddle PRIVATE FortranBottom)
+
+if(OPTIMIZE_TOP)
+  set_target_properties(FortranTop PROPERTIES
+    OPTIMIZE_DEPENDENCIES TRUE)
+endif()
+if(OPTIMIZE_MIDDLE)
+  set_target_properties(CMiddle PROPERTIES
+    OPTIMIZE_DEPENDENCIES TRUE)
+endif()
+
+include(WriteTargets.cmake)
+write_targets()
diff --git a/Tests/RunCMake/DependencyGraph/OptimizeShared-both-build-check.cmake b/Tests/RunCMake/DependencyGraph/OptimizeShared-both-build-check.cmake
new file mode 100644
index 0000000..312de04
--- /dev/null
+++ b/Tests/RunCMake/DependencyGraph/OptimizeShared-both-build-check.cmake
@@ -0,0 +1,11 @@
+include(${RunCMake_TEST_BINARY_DIR}/target_files.cmake)
+check_files(${RunCMake_TEST_BINARY_DIR}/out
+  ${SharedTop_TARGET_FILE}
+  ${SharedTop_TARGET_LINKER_FILE}
+  ${StaticMiddle_TARGET_FILE}
+  ${StaticNone_TARGET_FILE}
+  ${StaticPreBuild_TARGET_FILE}
+  ${StaticPreLink_TARGET_FILE}
+  ${StaticPostBuild_TARGET_FILE}
+  ${StaticCc_TARGET_FILE}
+  )
diff --git a/Tests/RunCMake/DependencyGraph/OptimizeShared-both.cmake b/Tests/RunCMake/DependencyGraph/OptimizeShared-both.cmake
new file mode 100644
index 0000000..c150e62
--- /dev/null
+++ b/Tests/RunCMake/DependencyGraph/OptimizeShared-both.cmake
@@ -0,0 +1 @@
+include(OptimizeCommon.cmake)
diff --git a/Tests/RunCMake/DependencyGraph/OptimizeShared-middle-build-check.cmake b/Tests/RunCMake/DependencyGraph/OptimizeShared-middle-build-check.cmake
new file mode 100644
index 0000000..312de04
--- /dev/null
+++ b/Tests/RunCMake/DependencyGraph/OptimizeShared-middle-build-check.cmake
@@ -0,0 +1,11 @@
+include(${RunCMake_TEST_BINARY_DIR}/target_files.cmake)
+check_files(${RunCMake_TEST_BINARY_DIR}/out
+  ${SharedTop_TARGET_FILE}
+  ${SharedTop_TARGET_LINKER_FILE}
+  ${StaticMiddle_TARGET_FILE}
+  ${StaticNone_TARGET_FILE}
+  ${StaticPreBuild_TARGET_FILE}
+  ${StaticPreLink_TARGET_FILE}
+  ${StaticPostBuild_TARGET_FILE}
+  ${StaticCc_TARGET_FILE}
+  )
diff --git a/Tests/RunCMake/DependencyGraph/OptimizeShared-middle.cmake b/Tests/RunCMake/DependencyGraph/OptimizeShared-middle.cmake
new file mode 100644
index 0000000..c150e62
--- /dev/null
+++ b/Tests/RunCMake/DependencyGraph/OptimizeShared-middle.cmake
@@ -0,0 +1 @@
+include(OptimizeCommon.cmake)
diff --git a/Tests/RunCMake/DependencyGraph/OptimizeShared-none-build-check.cmake b/Tests/RunCMake/DependencyGraph/OptimizeShared-none-build-check.cmake
new file mode 100644
index 0000000..312de04
--- /dev/null
+++ b/Tests/RunCMake/DependencyGraph/OptimizeShared-none-build-check.cmake
@@ -0,0 +1,11 @@
+include(${RunCMake_TEST_BINARY_DIR}/target_files.cmake)
+check_files(${RunCMake_TEST_BINARY_DIR}/out
+  ${SharedTop_TARGET_FILE}
+  ${SharedTop_TARGET_LINKER_FILE}
+  ${StaticMiddle_TARGET_FILE}
+  ${StaticNone_TARGET_FILE}
+  ${StaticPreBuild_TARGET_FILE}
+  ${StaticPreLink_TARGET_FILE}
+  ${StaticPostBuild_TARGET_FILE}
+  ${StaticCc_TARGET_FILE}
+  )
diff --git a/Tests/RunCMake/DependencyGraph/OptimizeShared-none.cmake b/Tests/RunCMake/DependencyGraph/OptimizeShared-none.cmake
new file mode 100644
index 0000000..c150e62
--- /dev/null
+++ b/Tests/RunCMake/DependencyGraph/OptimizeShared-none.cmake
@@ -0,0 +1 @@
+include(OptimizeCommon.cmake)
diff --git a/Tests/RunCMake/DependencyGraph/OptimizeShared-top-build-check.cmake b/Tests/RunCMake/DependencyGraph/OptimizeShared-top-build-check.cmake
new file mode 100644
index 0000000..312de04
--- /dev/null
+++ b/Tests/RunCMake/DependencyGraph/OptimizeShared-top-build-check.cmake
@@ -0,0 +1,11 @@
+include(${RunCMake_TEST_BINARY_DIR}/target_files.cmake)
+check_files(${RunCMake_TEST_BINARY_DIR}/out
+  ${SharedTop_TARGET_FILE}
+  ${SharedTop_TARGET_LINKER_FILE}
+  ${StaticMiddle_TARGET_FILE}
+  ${StaticNone_TARGET_FILE}
+  ${StaticPreBuild_TARGET_FILE}
+  ${StaticPreLink_TARGET_FILE}
+  ${StaticPostBuild_TARGET_FILE}
+  ${StaticCc_TARGET_FILE}
+  )
diff --git a/Tests/RunCMake/DependencyGraph/OptimizeShared-top.cmake b/Tests/RunCMake/DependencyGraph/OptimizeShared-top.cmake
new file mode 100644
index 0000000..c150e62
--- /dev/null
+++ b/Tests/RunCMake/DependencyGraph/OptimizeShared-top.cmake
@@ -0,0 +1 @@
+include(OptimizeCommon.cmake)
diff --git a/Tests/RunCMake/DependencyGraph/OptimizeStatic-both-build-check.cmake b/Tests/RunCMake/DependencyGraph/OptimizeStatic-both-build-check.cmake
new file mode 100644
index 0000000..5222ed7
--- /dev/null
+++ b/Tests/RunCMake/DependencyGraph/OptimizeStatic-both-build-check.cmake
@@ -0,0 +1,8 @@
+include(${RunCMake_TEST_BINARY_DIR}/target_files.cmake)
+check_files(${RunCMake_TEST_BINARY_DIR}/out
+  ${StaticTop_TARGET_FILE}
+  ${StaticPreBuild_TARGET_FILE}
+  ${StaticPreLink_TARGET_FILE}
+  ${StaticPostBuild_TARGET_FILE}
+  ${StaticCc_TARGET_FILE}
+  )
diff --git a/Tests/RunCMake/DependencyGraph/OptimizeStatic-both.cmake b/Tests/RunCMake/DependencyGraph/OptimizeStatic-both.cmake
new file mode 100644
index 0000000..c150e62
--- /dev/null
+++ b/Tests/RunCMake/DependencyGraph/OptimizeStatic-both.cmake
@@ -0,0 +1 @@
+include(OptimizeCommon.cmake)
diff --git a/Tests/RunCMake/DependencyGraph/OptimizeStatic-middle-build-check.cmake b/Tests/RunCMake/DependencyGraph/OptimizeStatic-middle-build-check.cmake
new file mode 100644
index 0000000..5cba223
--- /dev/null
+++ b/Tests/RunCMake/DependencyGraph/OptimizeStatic-middle-build-check.cmake
@@ -0,0 +1,10 @@
+include(${RunCMake_TEST_BINARY_DIR}/target_files.cmake)
+check_files(${RunCMake_TEST_BINARY_DIR}/out
+  ${StaticTop_TARGET_FILE}
+  ${StaticMiddle_TARGET_FILE}
+  ${StaticNone_TARGET_FILE}
+  ${StaticPreBuild_TARGET_FILE}
+  ${StaticPreLink_TARGET_FILE}
+  ${StaticPostBuild_TARGET_FILE}
+  ${StaticCc_TARGET_FILE}
+  )
diff --git a/Tests/RunCMake/DependencyGraph/OptimizeStatic-middle.cmake b/Tests/RunCMake/DependencyGraph/OptimizeStatic-middle.cmake
new file mode 100644
index 0000000..c150e62
--- /dev/null
+++ b/Tests/RunCMake/DependencyGraph/OptimizeStatic-middle.cmake
@@ -0,0 +1 @@
+include(OptimizeCommon.cmake)
diff --git a/Tests/RunCMake/DependencyGraph/OptimizeStatic-none-build-check.cmake b/Tests/RunCMake/DependencyGraph/OptimizeStatic-none-build-check.cmake
new file mode 100644
index 0000000..5cba223
--- /dev/null
+++ b/Tests/RunCMake/DependencyGraph/OptimizeStatic-none-build-check.cmake
@@ -0,0 +1,10 @@
+include(${RunCMake_TEST_BINARY_DIR}/target_files.cmake)
+check_files(${RunCMake_TEST_BINARY_DIR}/out
+  ${StaticTop_TARGET_FILE}
+  ${StaticMiddle_TARGET_FILE}
+  ${StaticNone_TARGET_FILE}
+  ${StaticPreBuild_TARGET_FILE}
+  ${StaticPreLink_TARGET_FILE}
+  ${StaticPostBuild_TARGET_FILE}
+  ${StaticCc_TARGET_FILE}
+  )
diff --git a/Tests/RunCMake/DependencyGraph/OptimizeStatic-none.cmake b/Tests/RunCMake/DependencyGraph/OptimizeStatic-none.cmake
new file mode 100644
index 0000000..c150e62
--- /dev/null
+++ b/Tests/RunCMake/DependencyGraph/OptimizeStatic-none.cmake
@@ -0,0 +1 @@
+include(OptimizeCommon.cmake)
diff --git a/Tests/RunCMake/DependencyGraph/OptimizeStatic-top-build-check.cmake b/Tests/RunCMake/DependencyGraph/OptimizeStatic-top-build-check.cmake
new file mode 100644
index 0000000..5222ed7
--- /dev/null
+++ b/Tests/RunCMake/DependencyGraph/OptimizeStatic-top-build-check.cmake
@@ -0,0 +1,8 @@
+include(${RunCMake_TEST_BINARY_DIR}/target_files.cmake)
+check_files(${RunCMake_TEST_BINARY_DIR}/out
+  ${StaticTop_TARGET_FILE}
+  ${StaticPreBuild_TARGET_FILE}
+  ${StaticPreLink_TARGET_FILE}
+  ${StaticPostBuild_TARGET_FILE}
+  ${StaticCc_TARGET_FILE}
+  )
diff --git a/Tests/RunCMake/DependencyGraph/OptimizeStatic-top.cmake b/Tests/RunCMake/DependencyGraph/OptimizeStatic-top.cmake
new file mode 100644
index 0000000..c150e62
--- /dev/null
+++ b/Tests/RunCMake/DependencyGraph/OptimizeStatic-top.cmake
@@ -0,0 +1 @@
+include(OptimizeCommon.cmake)
diff --git a/Tests/RunCMake/DependencyGraph/Property.cmake b/Tests/RunCMake/DependencyGraph/Property.cmake
new file mode 100644
index 0000000..08fdd2b
--- /dev/null
+++ b/Tests/RunCMake/DependencyGraph/Property.cmake
@@ -0,0 +1,24 @@
+enable_language(C)
+
+add_library(Unset STATIC mylib.c)
+
+set(CMAKE_OPTIMIZE_DEPENDENCIES TRUE)
+add_library(SetTrue STATIC mylib.c)
+
+set(CMAKE_OPTIMIZE_DEPENDENCIES FALSE)
+add_library(SetFalse STATIC mylib.c)
+
+get_property(_set TARGET Unset PROPERTY OPTIMIZE_DEPENDENCIES SET)
+if(_set)
+  message(SEND_ERROR "OPTIMIZE_DEPENDENCIES property should not be set on Unset target")
+endif()
+
+get_property(_true TARGET SetTrue PROPERTY OPTIMIZE_DEPENDENCIES)
+if(NOT _true STREQUAL "TRUE")
+  message(SEND_ERROR "OPTIMIZE_DEPENDENCIES property should be TRUE on SetTrue target")
+endif()
+
+get_property(_false TARGET SetFalse PROPERTY OPTIMIZE_DEPENDENCIES)
+if(NOT _false STREQUAL "FALSE")
+  message(SEND_ERROR "OPTIMIZE_DEPENDENCIES property should be FALSE on SetFalse target")
+endif()
diff --git a/Tests/RunCMake/DependencyGraph/RunCMakeTest.cmake b/Tests/RunCMake/DependencyGraph/RunCMakeTest.cmake
new file mode 100644
index 0000000..cb0d541
--- /dev/null
+++ b/Tests/RunCMake/DependencyGraph/RunCMakeTest.cmake
@@ -0,0 +1,60 @@
+include(RunCMake)
+
+function(check_files dir)
+  set(expected ${ARGN})
+  list(FILTER expected EXCLUDE REGEX "^$")
+  list(REMOVE_DUPLICATES expected)
+  list(SORT expected)
+
+  file(GLOB_RECURSE glob "${dir}/*")
+  set(actual)
+  foreach(i IN LISTS glob)
+    if(NOT i MATCHES "(\\.manifest$)|(\\.exp$)|(\\.tds$)")
+      list(APPEND actual ${i})
+    endif()
+  endforeach()
+  list(REMOVE_DUPLICATES actual)
+  list(SORT actual)
+
+  if(NOT "${expected}" STREQUAL "${actual}")
+    string(REPLACE ";" "\n  " expected_formatted "${expected}")
+    string(REPLACE ";" "\n  " actual_formatted "${actual}")
+    string(APPEND RunCMake_TEST_FAILED "Actual files did not match expected\nExpected:\n  ${expected_formatted}\nActual:\n  ${actual_formatted}\n")
+  endif()
+
+  set(RunCMake_TEST_FAILED "${RunCMake_TEST_FAILED}" PARENT_SCOPE)
+endfunction()
+
+function(run_cmake_build name)
+  set(RunCMake_TEST_NO_CLEAN TRUE)
+  set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/${name}-build)
+  file(REMOVE_RECURSE ${RunCMake_TEST_BINARY_DIR})
+  if(NOT RunCMake_GENERATOR_IS_MULTI_CONFIG)
+    list(APPEND RunCMake_TEST_OPTIONS -DCMAKE_BUILD_TYPE=Release)
+  endif()
+  run_cmake(${name})
+  set(RunCMake_TEST_OPTIONS)
+  run_cmake_command(${name}-build ${CMAKE_COMMAND}
+    --build ${RunCMake_TEST_BINARY_DIR}
+    --config Release
+    --target ${ARGN})
+endfunction()
+
+function(run_optimize_test name)
+  set(RunCMake_TEST_OPTIONS)
+  run_cmake_build(${name}-none ${ARGN})
+  set(RunCMake_TEST_OPTIONS -DOPTIMIZE_TOP=TRUE)
+  run_cmake_build(${name}-top ${ARGN})
+  set(RunCMake_TEST_OPTIONS -DOPTIMIZE_MIDDLE=TRUE)
+  run_cmake_build(${name}-middle ${ARGN})
+  set(RunCMake_TEST_OPTIONS -DOPTIMIZE_TOP=TRUE -DOPTIMIZE_MIDDLE=TRUE)
+  run_cmake_build(${name}-both ${ARGN})
+endfunction()
+
+run_cmake(Property)
+
+run_optimize_test(OptimizeShared SharedTop)
+run_optimize_test(OptimizeStatic StaticTop)
+if(CMAKE_Fortran_COMPILER)
+  run_optimize_test(OptimizeFortran FortranTop)
+endif()
diff --git a/Tests/RunCMake/DependencyGraph/WriteTargets.cmake b/Tests/RunCMake/DependencyGraph/WriteTargets.cmake
new file mode 100644
index 0000000..e1012c1
--- /dev/null
+++ b/Tests/RunCMake/DependencyGraph/WriteTargets.cmake
@@ -0,0 +1,16 @@
+function(write_targets)
+  set(_input "")
+
+  get_property(_targets DIRECTORY . PROPERTY BUILDSYSTEM_TARGETS)
+  foreach(_t IN LISTS _targets)
+    get_property(_type TARGET "${_t}" PROPERTY TYPE)
+    if(_type STREQUAL "SHARED_LIBRARY")
+      string(APPEND _input "set(${_t}_TARGET_FILE [==[$<TARGET_FILE:${_t}>]==])\n")
+      string(APPEND _input "set(${_t}_TARGET_LINKER_FILE [==[$<TARGET_LINKER_FILE:${_t}>]==])\n")
+    elseif(_type STREQUAL "STATIC_LIBRARY")
+      string(APPEND _input "set(${_t}_TARGET_FILE [==[$<TARGET_FILE:${_t}>]==])\n")
+    endif()
+  endforeach()
+
+  file(GENERATE OUTPUT target_files.cmake CONTENT "${_input}" CONDITION $<CONFIG:Release>)
+endfunction()
diff --git a/Tests/RunCMake/DependencyGraph/mylib.c b/Tests/RunCMake/DependencyGraph/mylib.c
new file mode 100644
index 0000000..5422fe3
--- /dev/null
+++ b/Tests/RunCMake/DependencyGraph/mylib.c
@@ -0,0 +1,6 @@
+#ifdef _WIN32
+__declspec(dllexport)
+#endif
+  void mylib(void)
+{
+}
diff --git a/Tests/RunCMake/DependencyGraph/mylib.f90 b/Tests/RunCMake/DependencyGraph/mylib.f90
new file mode 100644
index 0000000..104768f
--- /dev/null
+++ b/Tests/RunCMake/DependencyGraph/mylib.f90
@@ -0,0 +1,3 @@
+function mylib_fortran()
+  mylib_fortran = 42
+end function mylib_fortran