ya 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. #!/usr/bin/env python
  2. # Please, keep this script in sync with arcadia/ya
  3. import os
  4. import sys
  5. import platform
  6. RETRIES = 5
  7. HASH_PREFIX = 10
  8. REGISTRY_ENDPOINT = os.environ.get("YA_REGISTRY_ENDPOINT", "https://s3.mds.yandex.net/devtools-registry")
  9. # Please do not change this dict, it is updated automatically
  10. # Start of mapping
  11. PLATFORM_MAP = {
  12. "data": {
  13. "darwin": {
  14. "md5": "2acd2fce860fe3cea3384eec9483e655",
  15. "urls": [
  16. f"{REGISTRY_ENDPOINT}/4948877004"
  17. ]
  18. },
  19. "darwin-arm64": {
  20. "md5": "084a3e96c01df8ad5a086d124a7ded28",
  21. "urls": [
  22. f"{REGISTRY_ENDPOINT}/4948876606"
  23. ]
  24. },
  25. "linux-aarch64": {
  26. "md5": "4d017714a552c91c5c72ff925156f90b",
  27. "urls": [
  28. f"{REGISTRY_ENDPOINT}/4948875890"
  29. ]
  30. },
  31. "win32-clang-cl": {
  32. "md5": "518baa26eca7f2b5fc8fe6cfa3dba5fc",
  33. "urls": [
  34. f"{REGISTRY_ENDPOINT}/4948877440"
  35. ]
  36. },
  37. "linux": {
  38. "md5": "8eb7379c081b853abf9cf61a4870523b",
  39. "urls": [
  40. f"{REGISTRY_ENDPOINT}/4948877787"
  41. ]
  42. }
  43. }
  44. } # End of mapping
  45. def create_dirs(path):
  46. try:
  47. os.makedirs(path)
  48. except OSError as e:
  49. import errno
  50. if e.errno != errno.EEXIST:
  51. raise
  52. return path
  53. def home_dir():
  54. # Do not trust $HOME, as it is unreliable in certain environments
  55. # Temporarily delete os.environ["HOME"] to force reading current home directory from /etc/passwd
  56. home_from_env = os.environ.pop("HOME", None)
  57. try:
  58. home_from_passwd = os.path.expanduser("~")
  59. if os.path.isabs(home_from_passwd):
  60. # This home dir is valid, prefer it over $HOME
  61. return home_from_passwd
  62. else:
  63. # When python is built with musl (this is quire weird though),
  64. # only users from /etc/passwd will be properly resolved,
  65. # as musl does not have nss module for LDAP integration.
  66. return home_from_env
  67. finally:
  68. if home_from_env is not None:
  69. os.environ["HOME"] = home_from_env
  70. def misc_root():
  71. return create_dirs(os.getenv('YA_CACHE_DIR') or os.path.join(home_dir(), '.ya'))
  72. def tool_root():
  73. return create_dirs(os.getenv('YA_CACHE_DIR_TOOLS') or os.path.join(misc_root(), 'tools'))
  74. # TODO: remove when switched to S3, won't be needed in OSS
  75. def ya_token():
  76. def get_token_from_file():
  77. try:
  78. with open(os.environ.get('YA_TOKEN_PATH', os.path.join(home_dir(), '.ya_token')), 'r') as f:
  79. return f.read().strip()
  80. except:
  81. pass
  82. return os.getenv('YA_TOKEN') or get_token_from_file()
  83. TOOLS_DIR = tool_root()
  84. def uniq(size=6):
  85. import string
  86. import random
  87. return ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(size))
  88. _ssl_is_tuned = False
  89. def _tune_ssl():
  90. global _ssl_is_tuned
  91. if _ssl_is_tuned:
  92. return
  93. try:
  94. import ssl
  95. ssl._create_default_https_context = ssl._create_unverified_context
  96. except AttributeError:
  97. pass
  98. try:
  99. import urllib3
  100. urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
  101. except (AttributeError, ImportError):
  102. pass
  103. _ssl_is_tuned = True
  104. def _fetch(url, into):
  105. import hashlib
  106. _tune_ssl()
  107. from urllib.request import urlopen
  108. from urllib.request import Request
  109. from urllib.parse import urlparse
  110. request = Request(str(url))
  111. # TODO: Remove when switched to S3 distribution
  112. request.add_header('User-Agent', 'ya-bootstrap')
  113. token = ya_token()
  114. if token:
  115. request.add_header('Authorization', 'OAuth {}'.format(token))
  116. md5 = hashlib.md5()
  117. sys.stderr.write('Downloading %s ' % url)
  118. sys.stderr.flush()
  119. conn = urlopen(request, timeout=10)
  120. sys.stderr.write('[')
  121. sys.stderr.flush()
  122. try:
  123. with open(into, 'wb') as f:
  124. while True:
  125. block = conn.read(1024 * 1024)
  126. sys.stderr.write('.')
  127. sys.stderr.flush()
  128. if block:
  129. md5.update(block)
  130. f.write(block)
  131. else:
  132. break
  133. return md5.hexdigest()
  134. finally:
  135. sys.stderr.write('] ')
  136. sys.stderr.flush()
  137. def _atomic_fetch(url, into, md5):
  138. tmp_dest = into + '.' + uniq()
  139. try:
  140. real_md5 = _fetch(url, tmp_dest)
  141. if real_md5 != md5:
  142. raise Exception('MD5 mismatched: %s differs from %s' % (real_md5, md5))
  143. os.rename(tmp_dest, into)
  144. sys.stderr.write('OK\n')
  145. except Exception as e:
  146. sys.stderr.write('ERROR: ' + str(e) + '\n')
  147. raise
  148. finally:
  149. try:
  150. os.remove(tmp_dest)
  151. except OSError:
  152. pass
  153. def _extract(path, into):
  154. import tarfile
  155. tar = tarfile.open(path, errorlevel=2)
  156. # tar.extractall() will try to set file ownership according to the attributes stored in the archive
  157. # by calling TarFile.chown() method.
  158. # As this information is hardly relevant to the point of deployment / extraction,
  159. # it will just fail (python2) if ya is executed with root euid, or silently set non-existent numeric owner (python3)
  160. # to the files being extracted.
  161. # mock it with noop to retain current user ownership.
  162. tar.chown = lambda *args, **kwargs: None
  163. tar.extractall(path=into)
  164. tar.close()
  165. def _get(urls, md5):
  166. dest_path = os.path.join(TOOLS_DIR, md5[:HASH_PREFIX])
  167. if not os.path.exists(dest_path):
  168. for iter in range(RETRIES):
  169. try:
  170. _atomic_fetch(urls[iter % len(urls)], dest_path, md5)
  171. break
  172. except Exception:
  173. if iter + 1 == RETRIES:
  174. raise
  175. else:
  176. import time
  177. time.sleep(iter)
  178. return dest_path
  179. def _get_dir(urls, md5, ya_name):
  180. dest_dir = os.path.join(TOOLS_DIR, md5[:HASH_PREFIX] + '_d')
  181. if os.path.isfile(os.path.join(dest_dir, ya_name)):
  182. return dest_dir
  183. try:
  184. packed_path = _get(urls, md5)
  185. except Exception:
  186. if os.path.isfile(os.path.join(dest_dir, ya_name)):
  187. return dest_dir
  188. raise
  189. tmp_dir = dest_dir + '.' + uniq()
  190. try:
  191. try:
  192. _extract(packed_path, tmp_dir)
  193. except Exception:
  194. if os.path.isfile(os.path.join(dest_dir, ya_name)):
  195. return dest_dir
  196. raise
  197. try:
  198. os.rename(tmp_dir, dest_dir)
  199. except OSError as e:
  200. import errno
  201. if e.errno != errno.ENOTEMPTY:
  202. raise
  203. return dest_dir
  204. finally:
  205. import shutil
  206. shutil.rmtree(tmp_dir, ignore_errors=True)
  207. try:
  208. os.remove(packed_path)
  209. except Exception:
  210. pass
  211. def _mine_repo_root():
  212. # We think that this script is located in the root of the repo.
  213. return os.path.dirname(os.path.realpath(__file__))
  214. def main():
  215. if not os.path.exists(TOOLS_DIR):
  216. os.makedirs(TOOLS_DIR)
  217. result_args = sys.argv[1:]
  218. meta = PLATFORM_MAP['data']
  219. my_platform = platform.system().lower()
  220. my_machine = platform.machine().lower()
  221. if my_platform == 'linux':
  222. if 'ppc64le' in platform.platform():
  223. my_platform = 'linux-ppc64le'
  224. elif 'aarch64' in platform.platform():
  225. my_platform = 'linux-aarch64'
  226. else:
  227. my_platform = 'linux_musl'
  228. if my_platform == 'darwin' and my_machine == 'arm64':
  229. my_platform = 'darwin-arm64'
  230. def _platform_key(target_platform):
  231. """match by max prefix length, prefer shortest"""
  232. def _key_for_platform(platform):
  233. return len(os.path.commonprefix([target_platform, platform])), -len(platform)
  234. return _key_for_platform
  235. best_key = max(meta.keys(), key=_platform_key(my_platform))
  236. value = meta[best_key]
  237. ya_name = {'win32': 'ya-bin.exe', 'win32-clang-cl': 'ya-bin.exe'}.get(best_key, 'ya-bin') # XXX
  238. ya_dir = _get_dir(value['urls'], value['md5'], ya_name)
  239. # Popen `args` must have `str` type
  240. ya_path = str(os.path.join(ya_dir, ya_name))
  241. env = os.environ.copy()
  242. if 'YA_SOURCE_ROOT' not in env:
  243. src_root = _mine_repo_root()
  244. if src_root is not None:
  245. env['YA_SOURCE_ROOT'] = src_root
  246. for env_name in [
  247. 'LD_PRELOAD',
  248. 'Y_PYTHON_SOURCE_ROOT',
  249. ]:
  250. if env_name in os.environ:
  251. sys.stderr.write(
  252. "Warn: {}='{}' is specified and may affect the correct operation of the ya\n".format(
  253. env_name, env[env_name]
  254. )
  255. )
  256. if os.name == 'nt':
  257. import subprocess
  258. p = subprocess.Popen([ya_path] + result_args, env=env)
  259. p.wait()
  260. sys.exit(p.returncode)
  261. else:
  262. os.execve(ya_path, [ya_path] + result_args, env)
  263. if __name__ == '__main__':
  264. try:
  265. main()
  266. except Exception as e:
  267. sys.stderr.write('ERROR: ' + str(e) + '\n')
  268. from traceback import format_exc
  269. sys.stderr.write(format_exc() + "\n")
  270. sys.exit(1)