summaryrefslogtreecommitdiff
path: root/maint/check_toposort
blob: 491dfdfe029b465f9594f896d42dff1ac7e4b155 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
#!/usr/bin/env python3
#
# Helper script: makes sure that the crates as listed in the workspace Cargo.toml are
# topologically sorted from lowest-level to highest level.
#
# We depend on this property for publishing to crates.io. e.g.
# see
# https://blog.iany.me/2020/10/gotchas-to-publish-rust-crates-in-a-workspace/#cyclic-dependencies

import toml.decoder
import sys
import os.path
import os

TOPDIR = os.path.split(os.path.dirname(sys.argv[0]))[0]
CRATEDIR = os.path.join(TOPDIR, "crates")
WORKSPACE_TOML = os.path.join(TOPDIR, "Cargo.toml")


def crate_dirs():
    return set(name for name in os.listdir(CRATEDIR) if not name.startswith("."))


def strip_prefix(s, prefix):
    if s.startswith(prefix):
        return s[len(prefix) :]
    else:
        return s


def crate_list():
    t = toml.decoder.load(WORKSPACE_TOML)
    return list(
        strip_prefix(name, "crates/")
        for name in t["workspace"]["members"]
        if not (name.startswith("examples/") or name.startswith("maint/"))
    )


CRATE_LIST = crate_list()
CRATE_DIRS = crate_dirs()


def check_disjoint():
    listed_crates = set(crate_list())
    if listed_crates != CRATE_DIRS:
        print(
            "The crates in the crates/ directory do not match the ones in Cargo.toml!"
        )
        print("Problem crates", listed_crates ^ CRATE_DIRS)
        return True
    else:
        return False


def get_path(dep):
    try:
        return dep["path"]
    except (KeyError, TypeError):
        return None


def get_dependencies(cratename):
    fname = os.path.join(CRATEDIR, cratename, "Cargo.toml")
    t = toml.decoder.load(fname)
    deps = set()
    # We need to look at dev-dependencies too, and disallow false cyclic
    # dependencies through dev-dependencies.  We might be able to relax this if
    # crate publishing is updated to ignore dev-dependencies
    # <https://github.com/rust-lang/cargo/issues/4242>.
    for secname in ["dependencies", "dev-dependencies"]:
        sec = t.get(secname)
        if not sec:
            continue
        for key, val in sec.items():
            path = get_path(val)
            if path:
                d, p = os.path.split(val["path"])
                if d == "..":
                    assert p in CRATE_DIRS
                    deps.add(p)
    return deps


def get_dependency_graph():
    all_deps = {}
    for crate in CRATE_DIRS:
        all_deps[crate] = get_dependencies(crate)
    return all_deps


GRAPH = get_dependency_graph()


def check_consistency(order, graph):
    """Make sure that `order` is topologically sorted from bottom to
    top, according to `graph`.
    """
    seen_so_far = set()
    problems = False
    for crate in order:
        for dependent in graph[crate]:
            if dependent not in seen_so_far:
                print(
                    f"{crate} dependency on {dependent} is not reflected in Cargo.toml"
                )
                problems = True
        seen_so_far.add(crate)

    return problems


if __name__ == "__main__":
    if check_disjoint():
        sys.exit(1)
    if check_consistency(CRATE_LIST, GRAPH):
        sys.exit(1)
    print("Everything seems okay")