-
Notifications
You must be signed in to change notification settings - Fork 165
/
order-crates-for-publishing.py
executable file
·134 lines (112 loc) · 5.47 KB
/
order-crates-for-publishing.py
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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
#!/usr/bin/env python3
#
# This script figures the order in which workspace crates must be published to
# crates.io. Along the way it also ensures there are no circular dependencies
# that would cause a |cargo publish| to fail.
#
# On success an ordered list of Cargo.toml files is written to stdout
#
import os
import json
import subprocess
import sys;
real_file = os.path.realpath(__file__)
ci_path = os.path.dirname(real_file)
src_root = os.path.dirname(ci_path)
def load_metadata():
cmd = f'{src_root}/cargo metadata --no-deps --format-version=1'
return json.loads(subprocess.Popen(
cmd, shell=True, stdout=subprocess.PIPE).communicate()[0])
# Consider a situation where a crate now wants to use already existing
# developing-oriented library code for their integration tests and benchmarks,
# like creating malformed data or omitting signature verifications. Ideally,
# the code should have been guarded under the special feature
# `dev-context-only-utils` to avoid accidental misuse for production code path.
#
# In this case, the feature needs to be defined then activated for the crate
# itself. To that end, the crate actually needs to depend on itself as a
# dev-dependency with `dev-context-only-utils` activated, so that the feature
# is conditionally activated only for integration tests and benchmarks. In this
# way, other crates won't see the feature activated even if they normal-depend
# on the crate.
#
# This self-referencing dev-dependency can be thought of a variant of
# dev-dependency cycles and it's well supported by cargo. The only exception is
# when publishing. In general, cyclic dev-dependency doesn't work nicely with
# publishing: https://github.com/rust-lang/cargo/issues/4242 .
#
# However, there's a work around supported by cargo. Namely, it will ignore and
# strip these cyclic dev-dependencies when publishing, if explicit version
# isn't specified: https://github.com/rust-lang/cargo/pull/7333 (Released in
# rust 1.40.0: https://releases.rs/docs/1.40.0/#cargo )
#
# This script follows the same safe discarding logic to exclude these
# special-cased dev dependencies from its `dependency_graph` and further
# processing.
def is_self_dev_dep_with_dev_context_only_utils(package, dependency, wrong_self_dev_dependencies):
no_explicit_version = '*'
is_special_cased = False
if (dependency['kind'] == 'dev' and
dependency['name'] == package['name'] and
'dev-context-only-utils' in dependency['features'] and
'path' in dependency):
is_special_cased = True
if dependency['req'] != no_explicit_version:
# it's likely `{ workspace = true, ... }` is used, which implicitly pulls the
# version in...
wrong_self_dev_dependencies.append(dependency)
return is_special_cased
def should_add(package, dependency, wrong_self_dev_dependencies):
related_to_solana = dependency['name'].startswith('solana')
self_dev_dep_with_dev_context_only_utils = is_self_dev_dep_with_dev_context_only_utils(
package, dependency, wrong_self_dev_dependencies
)
return related_to_solana and not self_dev_dep_with_dev_context_only_utils
def get_packages():
metadata = load_metadata()
manifest_path = dict()
# Build dictionary of packages and their immediate solana-only dependencies
dependency_graph = dict()
wrong_self_dev_dependencies = list()
for pkg in metadata['packages']:
manifest_path[pkg['name']] = pkg['manifest_path'];
dependency_graph[pkg['name']] = [
x['name'] for x in pkg['dependencies'] if should_add(pkg, x, wrong_self_dev_dependencies)
];
# Check for direct circular dependencies
circular_dependencies = set()
for package, dependencies in dependency_graph.items():
for dependency in dependencies:
if dependency in dependency_graph and package in dependency_graph[dependency]:
circular_dependencies.add(' <--> '.join(sorted([package, dependency])))
for dependency in circular_dependencies:
sys.stderr.write('Error: Circular dependency: {}\n'.format(dependency))
for dependency in wrong_self_dev_dependencies:
sys.stderr.write('Error: wrong dev-context-only-utils circular dependency. try: ' +
'{} = {{ path = ".", features = {} }}\n'
.format(dependency['name'], json.dumps(dependency['features']))
)
if len(circular_dependencies) != 0 or len(wrong_self_dev_dependencies) != 0:
sys.exit(1)
# Order dependencies
sorted_dependency_graph = []
max_iterations = pow(len(dependency_graph),2)
while dependency_graph:
deleted_packages = []
if max_iterations == 0:
# One day be more helpful and find the actual cycle for the user...
sys.exit('Error: Circular dependency suspected between these packages: \n {}\n'.format('\n '.join(dependency_graph.keys())))
max_iterations -= 1
for package, dependencies in dependency_graph.items():
if package in deleted_packages:
continue
for dependency in dependencies:
if dependency in dependency_graph:
break
else:
deleted_packages.append(package)
sorted_dependency_graph.append((package, manifest_path[package]))
dependency_graph = {p: d for p, d in dependency_graph.items() if not p in deleted_packages }
return sorted_dependency_graph
for package, manifest in get_packages():
print(os.path.relpath(manifest))