/ maint / check_toposort
check_toposort
  1  #!/usr/bin/env python3
  2  #
  3  # Helper script: makes sure that the crates as listed in the workspace Cargo.toml are
  4  # topologically sorted from lowest-level to highest level.
  5  #
  6  # We depend on this property for publishing to crates.io. e.g.
  7  # see
  8  # https://blog.iany.me/2020/10/gotchas-to-publish-rust-crates-in-a-workspace/#cyclic-dependencies
  9  
 10  import toml.decoder
 11  import sys
 12  import os.path
 13  import os
 14  
 15  TOPDIR = os.path.split(os.path.dirname(sys.argv[0]))[0]
 16  CRATEDIR = os.path.join(TOPDIR, "crates")
 17  WORKSPACE_TOML = os.path.join(TOPDIR, "Cargo.toml")
 18  
 19  
 20  def crate_dirs():
 21      return set(name for name in os.listdir(CRATEDIR) if not name.startswith("."))
 22  
 23  
 24  def strip_prefix(s, prefix):
 25      if s.startswith(prefix):
 26          return s[len(prefix) :]
 27      else:
 28          return s
 29  
 30  
 31  def crate_list():
 32      t = toml.decoder.load(WORKSPACE_TOML)
 33      return list(
 34          strip_prefix(name, "crates/")
 35          for name in t["workspace"]["members"]
 36          if not (name.startswith("examples/") or name.startswith("maint/"))
 37      )
 38  
 39  
 40  CRATE_LIST = crate_list()
 41  CRATE_DIRS = crate_dirs()
 42  
 43  
 44  def check_disjoint():
 45      listed_crates = set(crate_list())
 46      if listed_crates != CRATE_DIRS:
 47          print(
 48              "The crates in the crates/ directory do not match the ones in Cargo.toml!"
 49          )
 50          print("Problem crates", listed_crates ^ CRATE_DIRS)
 51          return True
 52      else:
 53          return False
 54  
 55  
 56  def get_path(dep):
 57      try:
 58          return dep["path"]
 59      except (KeyError, TypeError):
 60          return None
 61  
 62  
 63  def get_dependencies(cratename):
 64      fname = os.path.join(CRATEDIR, cratename, "Cargo.toml")
 65      t = toml.decoder.load(fname)
 66      deps = set()
 67      # We need to look at dev-dependencies too, and disallow false cyclic
 68      # dependencies through dev-dependencies.  We might be able to relax this if
 69      # crate publishing is updated to ignore dev-dependencies
 70      # <https://github.com/rust-lang/cargo/issues/4242>.
 71      for secname in ["dependencies", "dev-dependencies"]:
 72          sec = t.get(secname)
 73          if not sec:
 74              continue
 75          for key, val in sec.items():
 76              path = get_path(val)
 77              if path:
 78                  d, p = os.path.split(val["path"])
 79                  if d == "..":
 80                      assert p in CRATE_DIRS
 81                      deps.add(p)
 82      return deps
 83  
 84  
 85  def get_dependency_graph():
 86      all_deps = {}
 87      for crate in CRATE_DIRS:
 88          all_deps[crate] = get_dependencies(crate)
 89      return all_deps
 90  
 91  
 92  GRAPH = get_dependency_graph()
 93  
 94  
 95  def check_consistency(order, graph):
 96      """Make sure that `order` is topologically sorted from bottom to
 97      top, according to `graph`.
 98      """
 99      seen_so_far = set()
100      problems = False
101      for crate in order:
102          for dependent in graph[crate]:
103              if dependent not in seen_so_far:
104                  print(
105                      f"{crate} dependency on {dependent} is not reflected in Cargo.toml"
106                  )
107                  problems = True
108          seen_so_far.add(crate)
109  
110      return problems
111  
112  
113  if __name__ == "__main__":
114      if check_disjoint():
115          sys.exit(1)
116      if check_consistency(CRATE_LIST, GRAPH):
117          sys.exit(1)
118      print("Everything seems okay")