cargo-outdated 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. #!/usr/bin/env python3
  2. import json
  3. import pickle
  4. import subprocess
  5. from argparse import ArgumentParser
  6. from os import chdir, getenv
  7. from os.path import exists, join
  8. from subprocess import PIPE
  9. import semver
  10. import tomlkit
  11. from colorama import Fore, Style
  12. from prettytable import PrettyTable
  13. # Path to git repository holding crates.io index
  14. CRATESIO_REPO = "https://github.com/rust-lang/crates.io-index"
  15. CRATESIO_INDEX = join(getenv("HOME"), ".cache", "crates.io-index")
  16. # Path to pickle cache for storing paths to dependency metadata
  17. PICKLE_CACHE = join(getenv("HOME"), ".cache", "cargo-outdated.pickle")
  18. # Set of packages that are ignored by this tool
  19. IGNORES = {
  20. "darkfi-serial",
  21. "darkfi-sdk",
  22. "darkfi",
  23. "darkfi-derive",
  24. "darkfi-derive-internal",
  25. "dao-contract",
  26. "money-contract",
  27. }
  28. # Yanked releases from crates.io to ignore
  29. YANKED = {}
  30. # Cached paths for metadata to not have to search through the crates index
  31. METADATA_PATHS = {}
  32. if exists(PICKLE_CACHE):
  33. with open(PICKLE_CACHE, "rb") as f:
  34. json_data = pickle.load(f)
  35. METADATA_PATHS = json.loads(json_data)
  36. def parse_toml(filename):
  37. with open(filename) as f:
  38. content = f.read()
  39. p = tomlkit.parse(content)
  40. deps = p.get("dependencies")
  41. devdeps = p.get("dev-dependencies")
  42. if deps and devdeps:
  43. dependencies = deps | devdeps
  44. elif deps:
  45. dependencies = deps
  46. elif devdeps:
  47. dependencies = devdeps
  48. else:
  49. dependencies = None
  50. return (p, dependencies)
  51. def get_metadata_path(name):
  52. find_output = subprocess.run(
  53. ["find", CRATESIO_INDEX, "-type", "f", "-name", name], stdout=PIPE)
  54. metadata_path = find_output.stdout.decode().strip()
  55. if metadata_path == '':
  56. return None
  57. # Place the path into cache
  58. METADATA_PATHS[name] = metadata_path
  59. return metadata_path
  60. def check_dep(name, data):
  61. if name in IGNORES:
  62. return None
  63. metadata_path = METADATA_PATHS.get(name)
  64. if not metadata_path:
  65. metadata_path = get_metadata_path(name)
  66. if not metadata_path:
  67. print(f"No crate found for {Fore.YELLOW}{name}{Style.RESET_ALL}")
  68. return None
  69. # Read the metadata. It's split as JSON objects, each in its own line.
  70. with open(metadata_path, encoding="utf-8") as f:
  71. lines = f.readlines()
  72. lines = [i.strip() for i in lines]
  73. # Latest one is at the end
  74. metadata = json.loads(lines[-1])
  75. # Get the version from the local data
  76. if isinstance(data, str):
  77. # This is just the semver
  78. local_version = data
  79. elif isinstance(data, dict):
  80. local_version = data.get("version")
  81. if not local_version:
  82. # Not a versioned dependency (can be path/git/...)
  83. return None
  84. else:
  85. raise ValueError(f"Invalid dependency: {name}")
  86. if semver.compare(local_version, metadata["vers"]) < 0:
  87. name = metadata["name"]
  88. vers = metadata["vers"]
  89. if name in YANKED and vers in YANKED[name]:
  90. return None
  91. return (local_version, vers)
  92. return None
  93. def main():
  94. parser = ArgumentParser(
  95. description="Prettyprint outdated dependencies in a cargo project")
  96. parser.add_argument("-u",
  97. "--update",
  98. action="store_true",
  99. help="Prompt to update dependencies")
  100. parser.add_argument("-i",
  101. "--ignore",
  102. type=str,
  103. help="Comma-separated list of deps to ignore")
  104. args = parser.parse_args()
  105. if args.ignore:
  106. for i in args.ignore.split(","):
  107. IGNORES.add(i)
  108. if not exists(CRATESIO_INDEX):
  109. print("Cloning crates.io index...")
  110. subprocess.run(["git", "clone", CRATESIO_REPO, CRATESIO_INDEX],
  111. capture_output=False)
  112. print("Updating crates.io index...")
  113. subprocess.run(["git", "-C", CRATESIO_INDEX, "fetch", "-a"],
  114. capture_output=False)
  115. subprocess.run(
  116. ["git", "-C", CRATESIO_INDEX, "reset", "--hard", "origin/master"],
  117. capture_output=False)
  118. # chdir to the root of the project
  119. toplevel = subprocess.run(["git", "rev-parse", "--show-toplevel"],
  120. capture_output=True)
  121. toplevel = toplevel.stdout.decode().strip()
  122. chdir(toplevel)
  123. find_output = subprocess.run(
  124. ["find", ".", "-type", "f", "-name", "Cargo.toml"], stdout=PIPE)
  125. files = [i.strip() for i in find_output.stdout.decode().split("\n")][:-1]
  126. x = PrettyTable()
  127. x.field_names = ["package", "crate", "current", "latest", "path"]
  128. for filename in files:
  129. ps, deps = parse_toml(filename)
  130. package = ps["package"]["name"]
  131. print(f"Checking deps for {Fore.GREEN}{package}{Style.RESET_ALL}")
  132. for dep in deps:
  133. ret = check_dep(dep, deps[dep])
  134. if ret:
  135. x.add_row([
  136. package,
  137. dep,
  138. f"{Fore.YELLOW}{ret[0]}{Style.RESET_ALL}",
  139. f"{Fore.GREEN}{ret[1]}{Style.RESET_ALL}",
  140. filename,
  141. ])
  142. if args.update and ret:
  143. print(f"Update {dep} from {ret[0]} to {ret[1]}? (y/N): ",
  144. end="")
  145. choice = input()
  146. if choice and (choice == "y" or choice == "Y"):
  147. if "dependencies" in ps and dep in ps["dependencies"]:
  148. if ps["dependencies"][dep] == ret[0]:
  149. ps["dependencies"][dep] = ret[1]
  150. else:
  151. ps["dependencies"][dep]["version"] = ret[1]
  152. elif "dev-dependencies" in ps and dep in ps[
  153. "dev-dependencies"]:
  154. if ps["dev-dependencies"][dep] == ret[0]:
  155. ps["dev-dependencies"][dep] = ret[1]
  156. else:
  157. ps["dev-dependencies"][dep]["version"] = ret[1]
  158. if args.update:
  159. with open(filename, "w") as f:
  160. f.write(tomlkit.dumps(ps))
  161. print(x)
  162. # Write the pickle
  163. with open(PICKLE_CACHE, "wb") as pfile:
  164. pickle.dump(json.dumps(METADATA_PATHS).encode(), pfile)
  165. if __name__ == "__main__":
  166. main()