freeze_requirements.py 4.7 KB

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