freeze_requirements.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. from __future__ import annotations
  2. import argparse
  3. from concurrent.futures import Future, ThreadPoolExecutor
  4. from os.path import abspath
  5. from subprocess import CalledProcessError, run
  6. from typing import Sequence
  7. from tools.lib import gitroot
  8. def worker(args: tuple[str, ...]) -> None:
  9. # pip-compile doesn't let you customize the header, so we write
  10. # one ourselves. However, pip-compile needs -o DEST otherwise
  11. # it will bump >= pins even if they're satisfied. So, we need to
  12. # unfortunately rewrite the whole file.
  13. dest = args[-1]
  14. try:
  15. run(args, check=True, capture_output=True)
  16. except CalledProcessError as e:
  17. raise e
  18. with open(dest, "rb+") as f:
  19. content = f.read()
  20. f.seek(0, 0)
  21. f.write(
  22. b"""# DO NOT MODIFY. This file was generated with `make freeze-requirements`.
  23. """
  24. + content
  25. )
  26. def check_futures(futures: list[Future[None]]) -> int:
  27. rc = 0
  28. for future in futures:
  29. try:
  30. future.result()
  31. except CalledProcessError as e:
  32. rc = 1
  33. print(
  34. f"""`{e.cmd}` returned code {e.returncode}
  35. stdout:
  36. {e.stdout.decode()}
  37. stderr:
  38. {e.stderr.decode()}
  39. """
  40. )
  41. return rc
  42. def main(argv: Sequence[str] | None = None) -> int:
  43. parser = argparse.ArgumentParser()
  44. parser.add_argument("repo", type=str, help="Repository name.")
  45. args = parser.parse_args(argv)
  46. repo = args.repo
  47. base_path = abspath(gitroot())
  48. base_cmd = (
  49. "pip-compile",
  50. "--allow-unsafe",
  51. "--no-annotate",
  52. "--no-header",
  53. "--quiet",
  54. "--strip-extras",
  55. "--index-url=https://pypi.devinfra.sentry.io/simple",
  56. )
  57. executor = ThreadPoolExecutor(max_workers=3)
  58. futures = []
  59. if repo != "getsentry":
  60. futures.append(
  61. executor.submit(
  62. worker,
  63. (
  64. *base_cmd,
  65. f"{base_path}/requirements-base.txt",
  66. "-o",
  67. f"{base_path}/requirements-frozen.txt",
  68. ),
  69. )
  70. )
  71. futures.append(
  72. executor.submit(
  73. worker,
  74. (
  75. *base_cmd,
  76. f"{base_path}/requirements-base.txt",
  77. f"{base_path}/requirements-dev.txt",
  78. "-o",
  79. f"{base_path}/requirements-dev-frozen.txt",
  80. ),
  81. )
  82. )
  83. else:
  84. futures.append(
  85. executor.submit(
  86. worker,
  87. (
  88. *base_cmd,
  89. f"{base_path}/requirements-base.txt",
  90. # This is downloaded with bin/bump-sentry.
  91. f"{base_path}/sentry-requirements-frozen.txt",
  92. "-o",
  93. f"{base_path}/requirements-frozen.txt",
  94. ),
  95. )
  96. )
  97. # getsentry shares sentry's requirements-dev.
  98. futures.append(
  99. executor.submit(
  100. worker,
  101. (
  102. *base_cmd,
  103. f"{base_path}/requirements-base.txt",
  104. # This is downloaded with bin/bump-sentry.
  105. f"{base_path}/sentry-requirements-dev-frozen.txt",
  106. "-o",
  107. f"{base_path}/requirements-dev-frozen.txt",
  108. ),
  109. )
  110. )
  111. if repo == "sentry":
  112. # requirements-dev-only-frozen.txt is only used in sentry
  113. # (and reused in getsentry) as a fast path for some CI jobs.
  114. futures.append(
  115. executor.submit(
  116. worker,
  117. (
  118. *base_cmd,
  119. f"{base_path}/requirements-dev.txt",
  120. "-o",
  121. f"{base_path}/requirements-dev-only-frozen.txt",
  122. ),
  123. )
  124. )
  125. rc = check_futures(futures)
  126. executor.shutdown()
  127. return rc
  128. if __name__ == "__main__":
  129. raise SystemExit(main())