| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216 |
- #!/usr/bin/env python3
- import json
- import pickle
- import subprocess
- from argparse import ArgumentParser
- from os import chdir, getenv
- from os.path import exists, join
- from subprocess import PIPE
- import semver
- import tomlkit
- from colorama import Fore, Style
- from prettytable import PrettyTable
- # Path to git repository holding crates.io index
- CRATESIO_REPO = "https://github.com/rust-lang/crates.io-index"
- CRATESIO_INDEX = join(getenv("HOME"), ".cache", "crates.io-index")
- # Path to pickle cache for storing paths to dependency metadata
- PICKLE_CACHE = join(getenv("HOME"), ".cache", "cargo-outdated.pickle")
- # Set of packages that are ignored by this tool
- IGNORES = {
- "darkfi-serial",
- "darkfi-sdk",
- "darkfi",
- "darkfi-derive",
- "darkfi-derive-internal",
- "dao-contract",
- "money-contract",
- }
- # Yanked releases from crates.io to ignore
- YANKED = {}
- # Cached paths for metadata to not have to search through the crates index
- METADATA_PATHS = {}
- if exists(PICKLE_CACHE):
- with open(PICKLE_CACHE, "rb") as f:
- json_data = pickle.load(f)
- METADATA_PATHS = json.loads(json_data)
- def parse_toml(filename):
- with open(filename) as f:
- content = f.read()
- p = tomlkit.parse(content)
- deps = p.get("dependencies")
- devdeps = p.get("dev-dependencies")
- if deps and devdeps:
- dependencies = deps | devdeps
- elif deps:
- dependencies = deps
- elif devdeps:
- dependencies = devdeps
- else:
- dependencies = None
- return (p, dependencies)
- def get_metadata_path(name):
- find_output = subprocess.run(
- ["find", CRATESIO_INDEX, "-type", "f", "-name", name], stdout=PIPE)
- metadata_path = find_output.stdout.decode().strip()
- if metadata_path == '':
- return None
- # Place the path into cache
- METADATA_PATHS[name] = metadata_path
- return metadata_path
- def check_dep(name, data):
- if name in IGNORES:
- return None
- metadata_path = METADATA_PATHS.get(name)
- if not metadata_path:
- metadata_path = get_metadata_path(name)
- if not metadata_path:
- print(f"No crate found for {Fore.YELLOW}{name}{Style.RESET_ALL}")
- return None
- # Read the metadata. It's split as JSON objects, each in its own line.
- with open(metadata_path, encoding="utf-8") as f:
- lines = f.readlines()
- lines = [i.strip() for i in lines]
- # Latest one is at the end
- metadata = json.loads(lines[-1])
- # Get the version from the local data
- if isinstance(data, str):
- # This is just the semver
- local_version = data
- elif isinstance(data, dict):
- local_version = data.get("version")
- if not local_version:
- # Not a versioned dependency (can be path/git/...)
- return None
- else:
- raise ValueError(f"Invalid dependency: {name}")
- try:
- if semver.compare(local_version, metadata["vers"]) < 0:
- name = metadata["name"]
- vers = metadata["vers"]
- if name in YANKED and vers in YANKED[name]:
- return None
- return (local_version, vers)
- except:
- return None
- return None
- def main():
- parser = ArgumentParser(
- description="Prettyprint outdated dependencies in a cargo project")
- parser.add_argument("-u",
- "--update",
- action="store_true",
- help="Prompt to update dependencies")
- parser.add_argument("-i",
- "--ignore",
- type=str,
- help="Comma-separated list of deps to ignore")
- args = parser.parse_args()
- if args.ignore:
- for i in args.ignore.split(","):
- IGNORES.add(i)
- if not exists(CRATESIO_INDEX):
- print("Cloning crates.io index...")
- subprocess.run(["git", "clone", CRATESIO_REPO, CRATESIO_INDEX],
- capture_output=False)
- print("Updating crates.io index...")
- subprocess.run(["git", "-C", CRATESIO_INDEX, "fetch", "-a"],
- capture_output=False)
- subprocess.run(
- ["git", "-C", CRATESIO_INDEX, "reset", "--hard", "origin/master"],
- capture_output=False)
- # chdir to the root of the project
- toplevel = subprocess.run(["git", "rev-parse", "--show-toplevel"],
- capture_output=True)
- toplevel = toplevel.stdout.decode().strip()
- chdir(toplevel)
- find_output = subprocess.run(
- ["find", ".", "-type", "f", "-name", "Cargo.toml"], stdout=PIPE)
- files = [i.strip() for i in find_output.stdout.decode().split("\n")][:-1]
- x = PrettyTable()
- x.field_names = ["package", "crate", "current", "latest", "path"]
- for filename in files:
- ps, deps = parse_toml(filename)
- package = ps["package"]["name"]
- print(f"Checking deps for {Fore.GREEN}{package}{Style.RESET_ALL}")
- for dep in deps:
- ret = check_dep(dep, deps[dep])
- if ret:
- x.add_row([
- package,
- dep,
- f"{Fore.YELLOW}{ret[0]}{Style.RESET_ALL}",
- f"{Fore.GREEN}{ret[1]}{Style.RESET_ALL}",
- filename,
- ])
- if args.update and ret:
- print(f"Update {dep} from {ret[0]} to {ret[1]}? (y/N): ",
- end="")
- choice = input()
- if choice and (choice == "y" or choice == "Y"):
- if "dependencies" in ps and dep in ps["dependencies"]:
- if ps["dependencies"][dep] == ret[0]:
- ps["dependencies"][dep] = ret[1]
- else:
- ps["dependencies"][dep]["version"] = ret[1]
- elif "dev-dependencies" in ps and dep in ps[
- "dev-dependencies"]:
- if ps["dev-dependencies"][dep] == ret[0]:
- ps["dev-dependencies"][dep] = ret[1]
- else:
- ps["dev-dependencies"][dep]["version"] = ret[1]
- if args.update:
- with open(filename, "w") as f:
- f.write(tomlkit.dumps(ps))
- print(x)
- # Write the pickle
- with open(PICKLE_CACHE, "wb") as pfile:
- pickle.dump(json.dumps(METADATA_PATHS).encode(), pfile)
- if __name__ == "__main__":
- main()
|