ya 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. #!/usr/bin/env sh
  2. # Shell commands follow
  3. # Next line is bilingual: it starts a comment in Python, but do nothing in shell
  4. """:"
  5. # Find a suitable python interpreter
  6. for cmd in python3 python; do
  7. command -v > /dev/null $cmd && exec `command -v $cmd` $0 "$@"
  8. done
  9. echo "Python interpreter is not found in this system, please, install python or contact DEVTOOLSSUPPORT" >2
  10. exit 2
  11. ":"""
  12. # Previous line is bilingual: it ends a comment in Python, but do nothing in shell
  13. # Shell commands end here
  14. # Python script follows
  15. import os
  16. import sys
  17. import platform
  18. import json
  19. URLS = ['https://proxy.sandbox.yandex-team.ru/4662278249', 'https://storage.yandex-team.ru/get-devtools/1937492/580ef3b4db20f34996620509680ef9e4/by_platform.json']
  20. MD5 = '580ef3b4db20f34996620509680ef9e4'
  21. URLS3 = ['https://proxy.sandbox.yandex-team.ru/4662302638', 'https://storage.yandex-team.ru/get-devtools/995452/802961a9c0a1db05263418e0e143b72c/by_platform.json']
  22. MD53 = '802961a9c0a1db05263418e0e143b72c'
  23. DEFAULT_PY_VER = 2
  24. RETRIES = 5
  25. HASH_PREFIX = 10
  26. PY3_HANDLERS = {
  27. "ya3bin0", "ya3bin3", # handers for tests
  28. "krevedko",
  29. "curl", "nvim", "gdb", "emacs", "grep", "jstyle", "nile", "sed", "vim",
  30. "py23_utils",
  31. 'ydb',
  32. }
  33. PY2_HANDLERS = {
  34. "ya2bin0", "ya2bin2",
  35. }
  36. EXPERIMENTAL_PY3_HANDLERS = set()
  37. DEVTOOLS_PY3_HANDLERS = {
  38. 'upload', 'download', 'paste', 'whoami',
  39. }
  40. def is_devtools():
  41. # type: () -> bool
  42. devtools_users = {'tldr', 'yetty', 'prettyboy', 'neksard', 'ival83', 'mikhnenko', 'aokhotin', 'somov', 'shadchin', 'albazh', 'grasscat', 'nogert', 'jolex007', 'aakuz', 'aripinen', 'sudilovskiy', 'tutelka', 'tutelka', 'vlasovskikh', 'vlasovskikh', 'and42', 'say', 'rudeshko', 'pg', 'artanis', 'hiddenpath', 'hiddenpath', 'khoden', 'bulatkhr', 'v-korovin', 'viknet', 'saxumcordis', 'vpozdyayev', 'vturov', 'vturov', 'vlad-savinov', 'slava', 'miripiruni', 'trushkin', 'griddic', 'ligreen', 'zaverden', 'kirpadmitriy', 'dmitko', 'dldmitry', 'lix0', 'yak-dmitriy', 'dankolesnikov', 'keepitsimple', 'workfork', 'vania-pooh', 'ilezhankin', 'igorart', 'thevery', 'ilia-vashurov', 'iaz1607', 'il2kondr4', 'ilyasiluyanov', 'trofimenkov', 'kirkharitonov', 'korum', 'tilacyn', 'maratik', 'miroslav2', 'spasitel', 'nslus', 'nsamokhin', 'r2d2', 'zhukoff-pavel', 'r-vetrov', 'gotocoding', 'svkov42', 'sabevzenko', 'belesev', 'svidyuk', 'snermolaev', 'spreis', 's-repalov', 'golovin-stan', 'ilikepugs', 'tekireeva', 'zumra6a', 'sareyu', 'thegeorg', 'snowball'}
  43. user = os.environ.get('USER')
  44. return bool(user and user in devtools_users)
  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. def ya_token():
  75. def get_token_from_file():
  76. try:
  77. with open(os.environ.get('YA_TOKEN_PATH', os.path.join(home_dir(), '.ya_token')), 'r') as f:
  78. return f.read().strip()
  79. except:
  80. pass
  81. return os.getenv('YA_TOKEN') or get_token_from_file()
  82. TOOLS_DIR = tool_root()
  83. def uniq(size=6):
  84. import string
  85. import random
  86. return ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(size))
  87. _ssl_is_tuned = False
  88. def _tune_ssl():
  89. global _ssl_is_tuned
  90. if _ssl_is_tuned:
  91. return
  92. try:
  93. import ssl
  94. ssl._create_default_https_context = ssl._create_unverified_context
  95. except AttributeError:
  96. pass
  97. try:
  98. import urllib3
  99. urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
  100. except (AttributeError, ImportError):
  101. pass
  102. _ssl_is_tuned = True
  103. def _fetch(url, into):
  104. import hashlib
  105. _tune_ssl()
  106. try:
  107. from urllib2 import urlopen
  108. from urllib2 import Request
  109. from urlparse import urlparse
  110. except ImportError:
  111. from urllib.request import urlopen
  112. from urllib.request import Request
  113. from urllib.parse import urlparse
  114. request = Request(str(url))
  115. request.add_header('User-Agent', 'ya-bootstrap')
  116. if urlparse(url).netloc == 'proxy.sandbox.yandex-team.ru':
  117. token = ya_token()
  118. if token:
  119. request.add_header('Authorization', 'OAuth {}'.format(token))
  120. md5 = hashlib.md5()
  121. sys.stderr.write('Downloading %s ' % url)
  122. sys.stderr.flush()
  123. conn = urlopen(request, timeout=10)
  124. sys.stderr.write('[')
  125. sys.stderr.flush()
  126. try:
  127. with open(into, 'wb') as f:
  128. while True:
  129. block = conn.read(1024 * 1024)
  130. sys.stderr.write('.')
  131. sys.stderr.flush()
  132. if block:
  133. md5.update(block)
  134. f.write(block)
  135. else:
  136. break
  137. return md5.hexdigest()
  138. finally:
  139. sys.stderr.write('] ')
  140. sys.stderr.flush()
  141. def _atomic_fetch(url, into, md5):
  142. tmp_dest = into + '.' + uniq()
  143. try:
  144. real_md5 = _fetch(url, tmp_dest)
  145. if real_md5 != md5:
  146. raise Exception('MD5 mismatched: %s differs from %s' % (real_md5, md5))
  147. os.rename(tmp_dest, into)
  148. sys.stderr.write('OK\n')
  149. except Exception as e:
  150. sys.stderr.write('ERROR: ' + str(e) + '\n')
  151. raise
  152. finally:
  153. try:
  154. os.remove(tmp_dest)
  155. except OSError:
  156. pass
  157. def _extract(path, into):
  158. import tarfile
  159. tar = tarfile.open(path, errorlevel=2)
  160. # tar.extractall() will try to set file ownership according to the attributes stored in the archive
  161. # by calling TarFile.chown() method.
  162. # As this information is hardly relevant to the point of deployment / extraction,
  163. # it will just fail (python2) if ya is executed with root euid, or silently set non-existent numeric owner (python3)
  164. # to the files being extracted.
  165. # mock it with noop to retain current user ownership.
  166. tar.chown = lambda *args, **kwargs: None
  167. tar.extractall(path=into)
  168. tar.close()
  169. def _get(urls, md5):
  170. dest_path = os.path.join(TOOLS_DIR, md5[:HASH_PREFIX])
  171. if not os.path.exists(dest_path):
  172. for iter in range(RETRIES):
  173. try:
  174. _atomic_fetch(urls[iter % len(urls)], dest_path, md5)
  175. break
  176. except Exception:
  177. if iter + 1 == RETRIES:
  178. raise
  179. else:
  180. import time
  181. time.sleep(iter)
  182. return dest_path
  183. def _get_dir(urls, md5, ya_name):
  184. dest_dir = os.path.join(TOOLS_DIR, md5[:HASH_PREFIX] + '_d')
  185. if os.path.isfile(os.path.join(dest_dir, ya_name)):
  186. return dest_dir
  187. try:
  188. packed_path = _get(urls, md5)
  189. except Exception:
  190. if os.path.isfile(os.path.join(dest_dir, ya_name)):
  191. return dest_dir
  192. raise
  193. tmp_dir = dest_dir + '.' + uniq()
  194. try:
  195. try:
  196. _extract(packed_path, tmp_dir)
  197. except Exception:
  198. if os.path.isfile(os.path.join(dest_dir, ya_name)):
  199. return dest_dir
  200. raise
  201. try:
  202. os.rename(tmp_dir, dest_dir)
  203. except OSError as e:
  204. import errno
  205. if e.errno != errno.ENOTEMPTY:
  206. raise
  207. return dest_dir
  208. finally:
  209. import shutil
  210. shutil.rmtree(tmp_dir, ignore_errors=True)
  211. try:
  212. os.remove(packed_path)
  213. except Exception:
  214. pass
  215. def _mine_arc_root():
  216. return os.path.dirname(os.path.realpath(__file__))
  217. def _parse_arguments():
  218. use_python = None
  219. use_python_set_force = False
  220. print_sandbox_id = False
  221. result_args = list(sys.argv[1:])
  222. handler = None
  223. if len(sys.argv) > 1:
  224. for index, arg in enumerate(sys.argv[1:]):
  225. if arg == "-3" or arg == "-2":
  226. if arg == "-3":
  227. new_value = 3
  228. elif arg == "-2":
  229. new_value = 2
  230. else:
  231. raise NotImplementedError("Unknown argument: {}".format(arg))
  232. if use_python is not None and use_python != new_value:
  233. sys.stderr.write("You can use only python2 (-2) OR python3 (-3) -based ya-bin, not both\n")
  234. exit(2)
  235. use_python = new_value
  236. use_python_set_force = True
  237. elif arg.startswith("--print-sandbox-id="):
  238. if print_sandbox_id:
  239. sys.stderr.write("You can print only one sandbox id at a time")
  240. exit(2)
  241. print_sandbox_id = arg.split('=')[1]
  242. else:
  243. # Do not try to parse remaining part of command
  244. result_args = result_args[index:]
  245. break
  246. # All ya script specific arguments found, search for handler
  247. skippable_flags = ('--error-file',)
  248. skip_next = False
  249. for arg in result_args:
  250. if not arg.startswith("-") and not skip_next:
  251. handler = arg
  252. break
  253. skip_next = arg in skippable_flags
  254. ENV_TRUE = ('yes', '1')
  255. py3_handlers_disabled = os.environ.get('YA_DISABLE_PY3_HANDLERS') in ENV_TRUE
  256. if py3_handlers_disabled:
  257. use_python = 2
  258. use_python_set_force = True # Prevent ya-bin respawn
  259. elif use_python == 3:
  260. if not print_sandbox_id:
  261. # will be shown only if user set `-3` by force
  262. sys.stderr.write("!! python3-based ya-bin will be used, "
  263. "be prepared for some strange effects, "
  264. "don't be ashamed to write in DEVTOOLSSUPPORT about it\n")
  265. pass
  266. elif use_python == 2:
  267. pass
  268. elif handler in PY2_HANDLERS:
  269. use_python = 2
  270. elif handler in PY3_HANDLERS:
  271. use_python = 3
  272. else:
  273. use_python = 23 # ya-bin/3 makes a decision
  274. if not use_python_set_force and not py3_handlers_disabled and use_python != 3:
  275. # User didn't set python version by force, didn't disable it, handler is under python2, so lets check experiments
  276. ya_experimental = os.environ.get("YA_EXPERIMENTAL") in ENV_TRUE
  277. if ya_experimental and handler in EXPERIMENTAL_PY3_HANDLERS:
  278. sys.stderr.write("!! python3-based ya-bin will be used because of:\n"
  279. " * You enable `YA_EXPERIMENTAL` environment variable\n"
  280. " * Handler `{}` in the list of experimental python3-compatible handlers\n"
  281. "".format(handler))
  282. use_python = 3
  283. elif is_devtools() and handler in DEVTOOLS_PY3_HANDLERS:
  284. sys.stderr.write("!! python3-based ya-bin will be used because of:\n"
  285. " * You are member of DEVTOOLS (it's just list of logins in `ya` script)\n"
  286. " * Handler `{}` in the list of experimental python3-compatible handlers for DEVTOOLS members\n"
  287. "".format(handler) )
  288. use_python = 3
  289. if use_python == 2:
  290. urls, md5 = URLS, MD5
  291. elif use_python == 3:
  292. urls, md5 = URLS3, MD53
  293. elif use_python == 23:
  294. if DEFAULT_PY_VER == 2:
  295. urls, md5 = URLS, MD5
  296. elif DEFAULT_PY_VER == 3:
  297. urls, md5 = URLS3, MD53
  298. else:
  299. raise NotImplementedError("Unknown default python version: {}".format(DEFAULT_PY_VER))
  300. else:
  301. raise NotImplementedError("Unknown python version: {}".format(use_python))
  302. return md5, print_sandbox_id, result_args, urls, use_python, use_python_set_force
  303. def main():
  304. if not os.path.exists(TOOLS_DIR):
  305. os.makedirs(TOOLS_DIR)
  306. md5, print_sandbox_id, result_args, urls, use_python, use_python_set_force = _parse_arguments()
  307. with open(_get(urls, md5), 'r') as fp:
  308. meta = json.load(fp)['data']
  309. my_platform = platform.system().lower()
  310. my_machine = platform.machine().lower()
  311. if my_platform == 'linux':
  312. if 'ppc64le' in platform.platform():
  313. my_platform = 'linux-ppc64le'
  314. elif 'aarch64' in platform.platform():
  315. my_platform = 'linux-aarch64'
  316. else:
  317. my_platform = 'linux_musl'
  318. if my_platform == 'darwin' and my_machine == 'arm64':
  319. my_platform = 'darwin-arm64'
  320. def _platform_key(target_platform):
  321. """ match by max prefix length, prefer shortest """
  322. def _key_for_platform(platform):
  323. return len(os.path.commonprefix([target_platform, platform])), -len(platform)
  324. return _key_for_platform
  325. best_key = max(meta.keys(), key=_platform_key(my_platform))
  326. value = meta[best_key]
  327. if print_sandbox_id:
  328. target = print_sandbox_id
  329. best_target = max(meta.keys(), key=_platform_key(target))
  330. sys.stdout.write(str(meta[best_target]['resource_id']) + '\n')
  331. exit(0)
  332. ya_name = {'win32': 'ya-bin.exe', 'win32-clang-cl': 'ya-bin.exe'}.get(best_key, 'ya-bin') # XXX
  333. ya_dir = _get_dir(value['urls'], value['md5'], ya_name)
  334. # Popen `args` must have `str` type
  335. ya_path = str(os.path.join(ya_dir, ya_name))
  336. env = os.environ.copy()
  337. if 'YA_SOURCE_ROOT' not in env:
  338. src_root = _mine_arc_root()
  339. if src_root is not None:
  340. env['YA_SOURCE_ROOT'] = src_root
  341. # For more info see YT-14105
  342. for env_name in [
  343. 'LD_PRELOAD',
  344. 'Y_PYTHON_SOURCE_ROOT',
  345. ]:
  346. if env_name in os.environ:
  347. sys.stderr.write("Warn: {}='{}' is specified and may affect the correct operation of the ya\n".format(env_name, env[env_name]))
  348. env['YA_PYVER_REQUIRE'] = str(use_python)
  349. if use_python_set_force:
  350. env['YA_PYVER_SET_FORCED'] = 'yes'
  351. if os.name == 'nt':
  352. import subprocess
  353. p = subprocess.Popen([ya_path] + result_args, env=env)
  354. p.wait()
  355. sys.exit(p.returncode)
  356. else:
  357. os.execve(ya_path, [ya_path] + result_args, env)
  358. if __name__ == '__main__':
  359. try:
  360. main()
  361. except Exception as e:
  362. sys.stderr.write('ERROR: ' + str(e) + '\n')
  363. from traceback import format_exc
  364. sys.stderr.write(format_exc() + "\n")
  365. sys.exit(1)