order-crates-for-publishing.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. #!/usr/bin/env python3
  2. #
  3. # This script figures the order in which workspace crates must be published to
  4. # crates.io. Along the way it also ensures there are no circular dependencies
  5. # that would cause a |cargo publish| to fail.
  6. #
  7. # On success an ordered list of Cargo.toml files is written to stdout
  8. #
  9. import os
  10. import json
  11. import subprocess
  12. import sys;
  13. real_file = os.path.realpath(__file__)
  14. ci_path = os.path.dirname(real_file)
  15. src_root = os.path.dirname(ci_path)
  16. def load_metadata():
  17. cmd = f'{src_root}/cargo metadata --no-deps --format-version=1'
  18. return json.loads(subprocess.Popen(
  19. cmd, shell=True, stdout=subprocess.PIPE).communicate()[0])
  20. # Consider a situation where a crate now wants to use already existing
  21. # developing-oriented library code for their integration tests and benchmarks,
  22. # like creating malformed data or omitting signature verifications. Ideally,
  23. # the code should have been guarded under the special feature
  24. # `dev-context-only-utils` to avoid accidental misuse for production code path.
  25. #
  26. # In this case, the feature needs to be defined then activated for the crate
  27. # itself. To that end, the crate actually needs to depend on itself as a
  28. # dev-dependency with `dev-context-only-utils` activated, so that the feature
  29. # is conditionally activated only for integration tests and benchmarks. In this
  30. # way, other crates won't see the feature activated even if they normal-depend
  31. # on the crate.
  32. #
  33. # This self-referencing dev-dependency can be thought of a variant of
  34. # dev-dependency cycles and it's well supported by cargo. The only exception is
  35. # when publishing. In general, cyclic dev-dependency doesn't work nicely with
  36. # publishing: https://github.com/rust-lang/cargo/issues/4242 .
  37. #
  38. # However, there's a work around supported by cargo. Namely, it will ignore and
  39. # strip these cyclic dev-dependencies when publishing, if explicit version
  40. # isn't specified: https://github.com/rust-lang/cargo/pull/7333 (Released in
  41. # rust 1.40.0: https://releases.rs/docs/1.40.0/#cargo )
  42. #
  43. # This script follows the same safe discarding logic to exclude these
  44. # special-cased dev dependencies from its `dependency_graph` and further
  45. # processing.
  46. def is_self_dev_dep_with_dev_context_only_utils(package, dependency, wrong_self_dev_dependencies):
  47. no_explicit_version = '*'
  48. is_special_cased = False
  49. if (dependency['kind'] == 'dev' and
  50. dependency['name'] == package['name'] and
  51. 'dev-context-only-utils' in dependency['features'] and
  52. 'path' in dependency):
  53. is_special_cased = True
  54. if dependency['req'] != no_explicit_version:
  55. # it's likely `{ workspace = true, ... }` is used, which implicitly pulls the
  56. # version in...
  57. wrong_self_dev_dependencies.append(dependency)
  58. return is_special_cased
  59. def should_add(package, dependency, wrong_self_dev_dependencies):
  60. related_to_solana = dependency['name'].startswith('solana')
  61. self_dev_dep_with_dev_context_only_utils = is_self_dev_dep_with_dev_context_only_utils(
  62. package, dependency, wrong_self_dev_dependencies
  63. )
  64. return related_to_solana and not self_dev_dep_with_dev_context_only_utils
  65. def get_packages():
  66. metadata = load_metadata()
  67. manifest_path = dict()
  68. # Build dictionary of packages and their immediate solana-only dependencies
  69. dependency_graph = dict()
  70. wrong_self_dev_dependencies = list()
  71. for pkg in metadata['packages']:
  72. manifest_path[pkg['name']] = pkg['manifest_path'];
  73. dependency_graph[pkg['name']] = [
  74. x['name'] for x in pkg['dependencies'] if should_add(pkg, x, wrong_self_dev_dependencies)
  75. ];
  76. # Check for direct circular dependencies
  77. circular_dependencies = set()
  78. for package, dependencies in dependency_graph.items():
  79. for dependency in dependencies:
  80. if dependency in dependency_graph and package in dependency_graph[dependency]:
  81. circular_dependencies.add(' <--> '.join(sorted([package, dependency])))
  82. for dependency in circular_dependencies:
  83. sys.stderr.write('Error: Circular dependency: {}\n'.format(dependency))
  84. for dependency in wrong_self_dev_dependencies:
  85. sys.stderr.write('Error: wrong dev-context-only-utils circular dependency. try: ' +
  86. '{} = {{ path = ".", features = {} }}\n'
  87. .format(dependency['name'], json.dumps(dependency['features']))
  88. )
  89. if len(circular_dependencies) != 0 or len(wrong_self_dev_dependencies) != 0:
  90. sys.exit(1)
  91. # Order dependencies
  92. sorted_dependency_graph = []
  93. max_iterations = pow(len(dependency_graph),2)
  94. while dependency_graph:
  95. deleted_packages = []
  96. if max_iterations == 0:
  97. # One day be more helpful and find the actual cycle for the user...
  98. sys.exit('Error: Circular dependency suspected between these packages: \n {}\n'.format('\n '.join(dependency_graph.keys())))
  99. max_iterations -= 1
  100. for package, dependencies in dependency_graph.items():
  101. if package in deleted_packages:
  102. continue
  103. for dependency in dependencies:
  104. if dependency in dependency_graph:
  105. break
  106. else:
  107. deleted_packages.append(package)
  108. sorted_dependency_graph.append((package, manifest_path[package]))
  109. dependency_graph = {p: d for p, d in dependency_graph.items() if not p in deleted_packages }
  110. return sorted_dependency_graph
  111. for package, manifest in get_packages():
  112. print(os.path.relpath(manifest))