pybuild.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823
  1. import collections
  2. import json
  3. import os
  4. import six
  5. from hashlib import md5
  6. import ymake
  7. from _common import stripext, rootrel_arc_src, listid, pathid, lazy, get_no_lint_value
  8. YA_IDE_VENV_VAR = 'YA_IDE_VENV'
  9. PY_NAMESPACE_PREFIX = 'py/namespace'
  10. BUILTIN_PROTO = 'builtin_proto'
  11. DEFAULT_FLAKE8_FILE_PROCESSING_TIME = "1.5" # in seconds
  12. DEFAULT_BLACK_FILE_PROCESSING_TIME = "1.5" # in seconds
  13. def _split_macro_call(macro_call, data, item_size, chunk_size=1024):
  14. index = 0
  15. length = len(data)
  16. offset = item_size * chunk_size
  17. while index + 1 < length:
  18. macro_call(data[index : index + offset])
  19. index += offset
  20. def is_arc_src(src, unit):
  21. return (
  22. src.startswith('${ARCADIA_ROOT}/')
  23. or src.startswith('${CURDIR}/')
  24. or unit.resolve_arc_path(src).startswith('$S/')
  25. )
  26. def is_extended_source_search_enabled(path, unit):
  27. if not is_arc_src(path, unit):
  28. return False
  29. if unit.get('NO_EXTENDED_SOURCE_SEARCH') == 'yes':
  30. return False
  31. # contrib is unfriendly to extended source search
  32. if unit.resolve_arc_path(path).startswith('$S/contrib/'):
  33. return False
  34. return True
  35. def to_build_root(path, unit):
  36. if is_arc_src(path, unit):
  37. return '${ARCADIA_BUILD_ROOT}/' + rootrel_arc_src(path, unit)
  38. return path
  39. def uniq_suffix(path, unit):
  40. upath = unit.path()
  41. if '/' not in path:
  42. return ''
  43. return '.{}'.format(pathid(upath)[:4])
  44. def pb2_arg(suf, path, mod, unit):
  45. return '{path}__int{py_ver}__{suf}={mod}{modsuf}'.format(
  46. path=stripext(to_build_root(path, unit)), suf=suf, mod=mod, modsuf=stripext(suf), py_ver=unit.get('_PYTHON_VER')
  47. )
  48. def proto_arg(path, mod, unit):
  49. return '{}.proto={}'.format(stripext(to_build_root(path, unit)), mod)
  50. def pb_cc_arg(suf, path, unit):
  51. return '{}{suf}'.format(stripext(to_build_root(path, unit)), suf=suf)
  52. def ev_cc_arg(path, unit):
  53. return '{}.ev.pb.cc'.format(stripext(to_build_root(path, unit)))
  54. def ev_arg(path, mod, unit):
  55. return '{}__int{}___ev_pb2.py={}_ev_pb2'.format(stripext(to_build_root(path, unit)), unit.get('_PYTHON_VER'), mod)
  56. def mangle(name):
  57. if '.' not in name:
  58. return name
  59. return ''.join('{}{}'.format(len(s), s) for s in name.split('.'))
  60. def parse_pyx_includes(filename, path, source_root, seen=None):
  61. def normpath(*args):
  62. return os.path.normpath(os.path.join(*args))
  63. abs_path = normpath(source_root, filename)
  64. seen = seen or set()
  65. if abs_path in seen:
  66. return
  67. seen.add(abs_path)
  68. if not os.path.exists(abs_path):
  69. # File might be missing, because it might be generated
  70. return
  71. with open(abs_path, 'rb') as f:
  72. # Don't parse cimports and etc - irrelevant for cython, it's linker work
  73. includes = [six.ensure_str(x) for x in ymake.parse_cython_includes(f.read())]
  74. abs_dirname = os.path.dirname(abs_path)
  75. # All includes are relative to the file which include
  76. path_dirname = os.path.dirname(path)
  77. file_dirname = os.path.dirname(filename)
  78. for incfile in includes:
  79. abs_path = normpath(abs_dirname, incfile)
  80. if os.path.exists(abs_path):
  81. incname, incpath = normpath(file_dirname, incfile), normpath(path_dirname, incfile)
  82. yield (incname, incpath)
  83. # search for includes in the included files
  84. for e in parse_pyx_includes(incname, incpath, source_root, seen):
  85. yield e
  86. else:
  87. # There might be arcadia root or cython relative include.
  88. # Don't treat such file as missing, because there must be PEERDIR on py_library
  89. # which contains it.
  90. for path in [
  91. source_root,
  92. source_root + "/contrib/tools/cython/Cython/Includes",
  93. ]:
  94. if os.path.exists(normpath(path, incfile)):
  95. break
  96. else:
  97. ymake.report_configure_error("'{}' includes missing file: {} ({})".format(path, incfile, abs_path))
  98. def has_pyx(args):
  99. return any(arg.endswith('.pyx') for arg in args)
  100. def get_srcdir(path, unit):
  101. return rootrel_arc_src(path, unit)[: -len(path)].rstrip('/')
  102. @lazy
  103. def get_ruff_configs(unit):
  104. rel_config_path = rootrel_arc_src(unit.get('RUFF_CONFIG_PATHS_FILE'), unit)
  105. arc_config_path = unit.resolve_arc_path(rel_config_path)
  106. abs_config_path = unit.resolve(arc_config_path)
  107. with open(abs_config_path, 'r') as fd:
  108. return list(json.load(fd).values())
  109. def add_python_lint_checks(unit, py_ver, files):
  110. @lazy
  111. def get_resolved_files():
  112. resolved_files = []
  113. for path in files:
  114. resolved = unit.resolve_arc_path([path])
  115. if resolved.startswith('$S'): # path was resolved as source file.
  116. resolved_files.append(resolved)
  117. return resolved_files
  118. upath = unit.path()[3:]
  119. no_lint_value = get_no_lint_value(unit)
  120. if no_lint_value == "none":
  121. no_lint_allowed_paths = (
  122. "contrib/",
  123. "devtools/",
  124. "junk/",
  125. # temporary allowed, TODO: remove
  126. "taxi/uservices/",
  127. "travel/",
  128. "market/report/lite/", # MARKETOUT-38662, deadline: 2021-08-12
  129. "passport/backend/oauth/", # PASSP-35982
  130. "testenv/", # CI-3229
  131. "yt/yt/", # YT-20053
  132. "yt/python/", # YT-20053
  133. )
  134. if not upath.startswith(no_lint_allowed_paths):
  135. ymake.report_configure_error("NO_LINT() is allowed only in " + ", ".join(no_lint_allowed_paths))
  136. if files and no_lint_value not in ("none", "none_internal"):
  137. resolved_files = get_resolved_files()
  138. if resolved_files:
  139. flake8_cfg = 'build/config/tests/flake8/flake8.conf'
  140. migrations_cfg = 'build/rules/flake8/migrations.yaml'
  141. resource = "build/external_resources/flake8_py{}".format(py_ver)
  142. lint_name = "py2_flake8" if py_ver == 2 else "flake8"
  143. params = [lint_name, "tools/flake8_linter/flake8_linter"]
  144. params += ["FILES"] + resolved_files
  145. params += ["GLOBAL_RESOURCES", resource]
  146. params += [
  147. "FILE_PROCESSING_TIME",
  148. unit.get("FLAKE8_FILE_PROCESSING_TIME") or DEFAULT_FLAKE8_FILE_PROCESSING_TIME,
  149. ]
  150. extra_params = []
  151. if unit.get("DISABLE_FLAKE8_MIGRATIONS") == "yes":
  152. extra_params.append("DISABLE_FLAKE8_MIGRATIONS=yes")
  153. config_files = [flake8_cfg, '']
  154. else:
  155. config_files = [flake8_cfg, migrations_cfg]
  156. params += ["CONFIGS"] + config_files
  157. if extra_params:
  158. params += ["EXTRA_PARAMS"] + extra_params
  159. unit.on_add_linter_check(params)
  160. # ruff related stuff
  161. if unit.get('STYLE_RUFF_VALUE') == 'yes':
  162. if no_lint_value in ("none", "none_internal"):
  163. ymake.report_configure_error(
  164. 'NO_LINT() and STYLE_RUFF() can\'t be enabled both at the same time',
  165. )
  166. resolved_files = get_resolved_files()
  167. if resolved_files:
  168. resource = "build/external_resources/ruff"
  169. params = ["ruff", "tools/ruff_linter/bin/ruff_linter"]
  170. params += ["FILES"] + resolved_files
  171. params += ["GLOBAL_RESOURCES", resource]
  172. configs = [
  173. rootrel_arc_src(unit.get('RUFF_CONFIG_PATHS_FILE'), unit),
  174. 'build/config/tests/ruff/ruff.toml',
  175. ] + get_ruff_configs(unit)
  176. params += ['CONFIGS'] + configs
  177. unit.on_add_linter_check(params)
  178. if files and unit.get('STYLE_PYTHON_VALUE') == 'yes' and is_py3(unit):
  179. resolved_files = get_resolved_files()
  180. if resolved_files:
  181. black_cfg = unit.get('STYLE_PYTHON_PYPROJECT_VALUE') or 'build/config/tests/py_style/config.toml'
  182. params = ['black', 'tools/black_linter/black_linter']
  183. params += ['FILES'] + resolved_files
  184. params += ['CONFIGS', black_cfg]
  185. params += [
  186. "FILE_PROCESSING_TIME",
  187. unit.get("BLACK_FILE_PROCESSING_TIME") or DEFAULT_BLACK_FILE_PROCESSING_TIME,
  188. ]
  189. unit.on_add_linter_check(params)
  190. def is_py3(unit):
  191. return unit.get("PYTHON3") == "yes"
  192. def on_py_program(unit, *args):
  193. py_program(unit, is_py3(unit))
  194. def py_program(unit, py3):
  195. """
  196. Documentation: https://wiki.yandex-team.ru/devtools/commandsandvars/py_srcs/#modulpyprogramimakrospymain
  197. """
  198. if py3:
  199. peers = ['library/python/runtime_py3/main']
  200. if unit.get('PYTHON_SQLITE3') != 'no':
  201. peers.append('contrib/tools/python3/Modules/_sqlite')
  202. else:
  203. peers = ['library/python/runtime/main']
  204. if unit.get('PYTHON_SQLITE3') != 'no':
  205. peers.append('contrib/tools/python/src/Modules/_sqlite')
  206. unit.onpeerdir(peers)
  207. if unit.get('MODULE_TYPE') == 'PROGRAM': # can not check DLL
  208. unit.onadd_check_py_imports()
  209. def onpy_srcs(unit, *args):
  210. """
  211. @usage PY_SRCS({| CYTHONIZE_PY} {| CYTHON_C} { | TOP_LEVEL | NAMESPACE ns} Files...)
  212. PY_SRCS() - is rule to build extended versions of Python interpreters and containing all application code in its executable file.
  213. It can be used to collect only the executables but not shared libraries, and, in particular, not to collect the modules that are imported using import directive.
  214. The main disadvantage is the lack of IDE support; There is also no readline yet.
  215. The application can be collect from any of the sources from which the C library, and with the help of PY_SRCS .py , .pyx,.proto and .swg files.
  216. At the same time extensions for Python on C language generating from .pyx and .swg, will be registered in Python's as built-in modules, and sources on .py are stored as static data:
  217. when the interpreter starts, the initialization code will add a custom loader of these modules to sys.meta_path.
  218. You can compile .py files as Cython sources with CYTHONIZE_PY directive (Use carefully, as build can get too slow). However, with it you won't have profiling info by default.
  219. To enable it, add "# cython: profile=True" line to the beginning of every cythonized source.
  220. By default .pyx files are collected as C++-extensions. To collect them as C (similar to BUILDWITH_CYTHON_C, but with the ability to specify namespace), you must specify the Directive CYTHON_C.
  221. Building with pyx automatically registers modules, you do not need to call PY_REGISTER for them
  222. __init__.py never required, but if present (and specified in PY_SRCS), it will be imported when you import package modules with __init__.py Oh.
  223. Example of library declaration with PY_SRCS():
  224. PY2_LIBRARY(mymodule)
  225. PY_SRCS(a.py sub/dir/b.py e.proto sub/dir/f.proto c.pyx sub/dir/d.pyx g.swg sub/dir/h.swg)
  226. END()
  227. PY_REGISTER honors Python2 and Python3 differences and adjusts itself to Python version of a current module
  228. Documentation: https://wiki.yandex-team.ru/arcadia/python/pysrcs/#modulipylibrarypy3libraryimakrospysrcs
  229. """
  230. # Each file arg must either be a path, or "${...}/buildpath=modname", where
  231. # "${...}/buildpath" part will be used as a file source in a future macro,
  232. # and "modname" will be used as a module name.
  233. upath = unit.path()[3:]
  234. py3 = is_py3(unit)
  235. py_main_only = unit.get('PROCESS_PY_MAIN_ONLY')
  236. with_py = not unit.get('PYBUILD_NO_PY')
  237. with_pyc = not unit.get('PYBUILD_NO_PYC')
  238. in_proto_library = unit.get('PY_PROTO') or unit.get('PY3_PROTO')
  239. venv = unit.get(YA_IDE_VENV_VAR)
  240. need_gazetteer_peerdir = False
  241. trim = 0
  242. if (
  243. not upath.startswith('contrib/tools/python')
  244. and not upath.startswith('library/python/runtime')
  245. and unit.get('NO_PYTHON_INCLS') != 'yes'
  246. ):
  247. unit.onpeerdir(['contrib/libs/python'])
  248. unit_needs_main = unit.get('MODULE_TYPE') in ('PROGRAM', 'DLL')
  249. if unit_needs_main:
  250. py_program(unit, py3)
  251. py_namespace_value = unit.get('PY_NAMESPACE_VALUE')
  252. if py_namespace_value == ".":
  253. ns = ""
  254. else:
  255. ns = (unit.get('PY_NAMESPACE_VALUE') or upath.replace('/', '.')) + '.'
  256. cython_coverage = unit.get('CYTHON_COVERAGE') == 'yes'
  257. cythonize_py = False
  258. optimize_proto = unit.get('OPTIMIZE_PY_PROTOS_FLAG') == 'yes'
  259. cython_directives = []
  260. if cython_coverage:
  261. cython_directives += ['-X', 'linetrace=True']
  262. pyxs_c = []
  263. pyxs_c_h = []
  264. pyxs_c_api_h = []
  265. pyxs_cpp = []
  266. pyxs_cpp_h = []
  267. pyxs = pyxs_cpp
  268. swigs_c = []
  269. swigs_cpp = []
  270. swigs = swigs_cpp
  271. pys = []
  272. pyis = []
  273. protos = []
  274. evs = []
  275. fbss = []
  276. py_namespaces = {}
  277. dump_dir = unit.get('PYTHON_BUILD_DUMP_DIR')
  278. dump_output = None
  279. if dump_dir:
  280. import threading
  281. pid = os.getpid()
  282. tid = threading.current_thread().ident
  283. dump_name = '{}-{}.dump'.format(pid, tid)
  284. dump_output = open(os.path.join(dump_dir, dump_name), 'a')
  285. args = iter(args)
  286. for arg in args:
  287. # Namespace directives.
  288. if arg == 'TOP_LEVEL':
  289. ns = ''
  290. elif arg == 'NAMESPACE':
  291. ns = next(args) + '.'
  292. # Cython directives.
  293. elif arg == 'CYTHON_C':
  294. pyxs = pyxs_c
  295. elif arg == 'CYTHON_C_H':
  296. pyxs = pyxs_c_h
  297. elif arg == 'CYTHON_C_API_H':
  298. pyxs = pyxs_c_api_h
  299. elif arg == 'CYTHON_CPP':
  300. pyxs = pyxs_cpp
  301. elif arg == 'CYTHON_CPP_H':
  302. pyxs = pyxs_cpp_h
  303. elif arg == 'CYTHON_DIRECTIVE':
  304. cython_directives += ['-X', next(args)]
  305. elif arg == 'CYTHONIZE_PY':
  306. cythonize_py = True
  307. # SWIG.
  308. elif arg == 'SWIG_C':
  309. swigs = swigs_c
  310. elif arg == 'SWIG_CPP':
  311. swigs = swigs_cpp
  312. # Unsupported but legal PROTO_LIBRARY arguments.
  313. elif arg == 'GLOBAL' or not in_proto_library and arg.endswith('.gztproto'):
  314. pass
  315. elif arg == '_MR':
  316. # GLOB support: convert arcadia-root-relative paths to module-relative
  317. # srcs are assumed to start with ${ARCADIA_ROOT}
  318. trim = len(unit.path()) + 14
  319. # Sources.
  320. else:
  321. main_mod = arg == 'MAIN'
  322. if main_mod:
  323. arg = next(args)
  324. if '=' in arg:
  325. main_py = False
  326. path, mod = arg.split('=', 1)
  327. else:
  328. if trim:
  329. arg = arg[trim:]
  330. if arg.endswith('.gztproto'):
  331. need_gazetteer_peerdir = True
  332. path = '{}.proto'.format(arg[:-9])
  333. else:
  334. path = arg
  335. main_py = path == '__main__.py' or path.endswith('/__main__.py')
  336. if not py3 and unit_needs_main and main_py:
  337. mod = '__main__'
  338. else:
  339. if arg.startswith('../'):
  340. ymake.report_configure_error('PY_SRCS item starts with "../": {!r}'.format(arg))
  341. if arg.startswith('/'):
  342. ymake.report_configure_error('PY_SRCS item starts with "/": {!r}'.format(arg))
  343. continue
  344. mod_name = stripext(arg).replace('/', '.')
  345. if py3 and path.endswith('.py') and is_extended_source_search_enabled(path, unit):
  346. # Dig out real path from the file path. Unit.path is not enough because of SRCDIR and ADDINCL
  347. root_rel_path = rootrel_arc_src(path, unit)
  348. mod_root_path = root_rel_path[: -(len(path) + 1)]
  349. py_namespaces.setdefault(mod_root_path, set()).add(ns if ns else '.')
  350. mod = ns + mod_name
  351. if in_proto_library:
  352. mod = mod.replace('-', '_')
  353. if main_mod:
  354. py_main(unit, mod + ":main")
  355. elif py3 and unit_needs_main and main_py:
  356. py_main(unit, mod)
  357. if py_main_only:
  358. continue
  359. if py3 and mod == '__main__':
  360. ymake.report_configure_error('TOP_LEVEL __main__.py is not allowed in PY3_PROGRAM')
  361. pathmod = (path, mod)
  362. if dump_output is not None:
  363. dump_output.write(
  364. '{path}\t{module}\t{py3}\n'.format(
  365. path=rootrel_arc_src(path, unit), module=mod, py3=1 if py3 else 0
  366. )
  367. )
  368. if path.endswith('.py'):
  369. if cythonize_py:
  370. pyxs.append(pathmod)
  371. else:
  372. pys.append(pathmod)
  373. elif path.endswith('.pyx'):
  374. pyxs.append(pathmod)
  375. elif path.endswith('.proto'):
  376. protos.append(pathmod)
  377. elif path.endswith('.ev'):
  378. evs.append(pathmod)
  379. elif path.endswith('.swg'):
  380. swigs.append(pathmod)
  381. elif path.endswith('.pyi'):
  382. pyis.append(pathmod)
  383. elif path.endswith('.fbs'):
  384. fbss.append(pathmod)
  385. else:
  386. ymake.report_configure_error('in PY_SRCS: unrecognized arg {!r}'.format(path))
  387. if dump_output is not None:
  388. dump_output.close()
  389. if pyxs:
  390. py_files2res = set()
  391. cpp_files2res = set()
  392. # Include map stores files which were included in the processing pyx file,
  393. # to be able to find source code of the included file inside generated file
  394. # for currently processing pyx file.
  395. include_map = collections.defaultdict(set)
  396. if cython_coverage:
  397. def process_pyx(filename, path, out_suffix, with_ext):
  398. # skip generated files
  399. if not is_arc_src(path, unit):
  400. return
  401. # source file
  402. py_files2res.add((filename, path))
  403. # generated
  404. if with_ext is None:
  405. cpp_files2res.add(
  406. (
  407. os.path.splitext(filename)[0] + out_suffix,
  408. os.path.splitext(path)[0] + out_suffix,
  409. )
  410. )
  411. else:
  412. cpp_files2res.add((filename + with_ext + out_suffix, path + with_ext + out_suffix))
  413. # used includes
  414. for entry in parse_pyx_includes(filename, path, unit.resolve('$S')):
  415. py_files2res.add(entry)
  416. include_arc_rel = entry[0]
  417. include_map[filename].add(include_arc_rel)
  418. else:
  419. def process_pyx(filename, path, out_suffix, with_ext):
  420. pass
  421. obj_suff = unit.get('OBJ_SUF')
  422. assert obj_suff is not None
  423. for pyxs, cython, out_suffix, with_ext in [
  424. (pyxs_c, unit.on_buildwith_cython_c_dep, ".c", obj_suff),
  425. (pyxs_c_h, unit.on_buildwith_cython_c_h, ".c", None),
  426. (pyxs_c_api_h, unit.on_buildwith_cython_c_api_h, ".c", None),
  427. (pyxs_cpp, unit.on_buildwith_cython_cpp_dep, ".cpp", obj_suff),
  428. (pyxs_cpp_h, unit.on_buildwith_cython_cpp_h, ".cpp", None),
  429. ]:
  430. for path, mod in pyxs:
  431. filename = rootrel_arc_src(path, unit)
  432. cython_args = [path]
  433. dep = path
  434. if path.endswith('.py'):
  435. pxd = '/'.join(mod.split('.')) + '.pxd'
  436. if unit.resolve_arc_path(pxd):
  437. dep = pxd
  438. cython_args.append(dep)
  439. cython_args += [
  440. '--module-name',
  441. mod,
  442. '--init-suffix',
  443. mangle(mod),
  444. '--source-root',
  445. '${ARCADIA_ROOT}',
  446. # set arcadia root relative __file__ for generated modules
  447. '-X',
  448. 'set_initial_path={}'.format(filename),
  449. ] + cython_directives
  450. cython(cython_args)
  451. py_register(unit, mod, py3)
  452. process_pyx(filename, path, out_suffix, with_ext)
  453. if cythonize_py:
  454. # Lint checks are not added for cythonized files by default, so we must add it here
  455. # as we are doing for regular pys.
  456. _23 = 3 if py3 else 2
  457. add_python_lint_checks(
  458. unit,
  459. _23,
  460. [path for path, mod in pyxs if path.endswith(".py")]
  461. + unit.get(['_PY_EXTRA_LINT_FILES_VALUE']).split(),
  462. )
  463. if py_files2res:
  464. # Compile original and generated sources into target for proper cython coverage calculation
  465. for files2res in (py_files2res, cpp_files2res):
  466. unit.onresource_files([x for name, path in files2res for x in ('DEST', name, path)])
  467. if include_map:
  468. data = []
  469. prefix = 'resfs/cython/include'
  470. for line in sorted(
  471. '{}/{}={}'.format(prefix, filename, ':'.join(sorted(files)))
  472. for filename, files in six.iteritems(include_map)
  473. ):
  474. data += ['-', line]
  475. unit.onresource(data)
  476. for swigs, on_swig_python in [
  477. (swigs_c, unit.on_swig_python_c),
  478. (swigs_cpp, unit.on_swig_python_cpp),
  479. ]:
  480. for path, mod in swigs:
  481. # Make output prefix basename match swig module name.
  482. prefix = path[: path.rfind('/') + 1] + mod.rsplit('.', 1)[-1]
  483. swg_py = '{}/{}/{}.py'.format('${ARCADIA_BUILD_ROOT}', upath, prefix)
  484. on_swig_python([path, prefix])
  485. onpy_register(unit, mod + '_swg')
  486. onpy_srcs(unit, swg_py + '=' + mod)
  487. if pys:
  488. pys_seen = set()
  489. pys_dups = {m for _, m in pys if (m in pys_seen or pys_seen.add(m))}
  490. if pys_dups:
  491. ymake.report_configure_error('Duplicate(s) is found in the PY_SRCS macro: {}'.format(pys_dups))
  492. res = []
  493. if py3:
  494. mod_list_md5 = md5()
  495. for path, mod in pys:
  496. mod_list_md5.update(six.ensure_binary(mod))
  497. if not (venv and is_extended_source_search_enabled(path, unit)):
  498. dest = 'py/' + mod.replace('.', '/') + '.py'
  499. if with_py:
  500. res += ['DEST', dest, path]
  501. if with_pyc:
  502. root_rel_path = rootrel_arc_src(path, unit)
  503. dst = path + uniq_suffix(path, unit)
  504. unit.on_py3_compile_bytecode([root_rel_path + '-', path, dst])
  505. res += ['DEST', dest + '.yapyc3', dst + '.yapyc3']
  506. if py_namespaces:
  507. # Note: Add md5 to key to prevent key collision if two or more PY_SRCS() used in the same ya.make
  508. ns_res = []
  509. for path, ns in sorted(py_namespaces.items()):
  510. key = '{}/{}/{}'.format(PY_NAMESPACE_PREFIX, mod_list_md5.hexdigest(), path)
  511. namespaces = ':'.join(sorted(ns))
  512. ns_res += ['-', '{}="{}"'.format(key, namespaces)]
  513. unit.onresource(ns_res)
  514. _split_macro_call(unit.onresource_files, res, (3 if with_py else 0) + (3 if with_pyc else 0))
  515. add_python_lint_checks(
  516. unit, 3, [path for path, mod in pys] + unit.get(['_PY_EXTRA_LINT_FILES_VALUE']).split()
  517. )
  518. else:
  519. for path, mod in pys:
  520. root_rel_path = rootrel_arc_src(path, unit)
  521. if with_py:
  522. key = '/py_modules/' + mod
  523. res += [
  524. path,
  525. key,
  526. '-',
  527. 'resfs/src/{}={}'.format(key, root_rel_path),
  528. ]
  529. if with_pyc:
  530. src = unit.resolve_arc_path(path) or path
  531. dst = path + uniq_suffix(path, unit)
  532. unit.on_py_compile_bytecode([root_rel_path + '-', src, dst])
  533. res += [dst + '.yapyc', '/py_code/' + mod]
  534. _split_macro_call(unit.onresource, res, (4 if with_py else 0) + (2 if with_pyc else 0))
  535. add_python_lint_checks(
  536. unit, 2, [path for path, mod in pys] + unit.get(['_PY_EXTRA_LINT_FILES_VALUE']).split()
  537. )
  538. if pyis:
  539. pyis_seen = set()
  540. pyis_dups = {m for _, m in pyis if (m in pyis_seen or pyis_seen.add(m))}
  541. if pyis_dups:
  542. pyis_dups = ', '.join(name for name in sorted(pyis_dups))
  543. ymake.report_configure_error('Duplicate(s) is found in the PY_SRCS macro: {}'.format(pyis_dups))
  544. res = []
  545. for path, mod in pyis:
  546. dest = 'py/' + mod.replace('.', '/') + '.pyi'
  547. res += ['DEST', dest, path]
  548. unit.onresource_files(res)
  549. use_vanilla_protoc = unit.get('USE_VANILLA_PROTOC') == 'yes'
  550. if use_vanilla_protoc:
  551. cpp_runtime_path = 'contrib/libs/protobuf_std'
  552. py_runtime_path = 'contrib/python/protobuf_std'
  553. builtin_proto_path = cpp_runtime_path + '/' + BUILTIN_PROTO
  554. else:
  555. cpp_runtime_path = 'contrib/libs/protobuf'
  556. py_runtime_path = 'contrib/python/protobuf'
  557. builtin_proto_path = cpp_runtime_path + '/' + BUILTIN_PROTO
  558. if protos:
  559. if not upath.startswith(py_runtime_path) and not upath.startswith(builtin_proto_path):
  560. if 'protobuf_old' not in upath:
  561. unit.onpeerdir(py_runtime_path)
  562. unit.onpeerdir(unit.get("PY_PROTO_DEPS").split())
  563. proto_paths = [path for path, mod in protos]
  564. unit.on_generate_py_protos_internal(proto_paths)
  565. unit.onpy_srcs(
  566. [
  567. pb2_arg(py_suf, path, mod, unit)
  568. for path, mod in protos
  569. for py_suf in unit.get("PY_PROTO_SUFFIXES").split()
  570. ]
  571. )
  572. if optimize_proto and need_gazetteer_peerdir:
  573. unit.onpeerdir(['kernel/gazetteer/proto'])
  574. if evs:
  575. unit.onpeerdir([cpp_runtime_path])
  576. unit.on_generate_py_evs_internal([path for path, mod in evs])
  577. unit.onpy_srcs([ev_arg(path, mod, unit) for path, mod in evs])
  578. if fbss:
  579. unit.onpeerdir(unit.get('_PY_FBS_DEPS').split())
  580. pysrc_base_name = listid(fbss)
  581. if py3:
  582. unit.onfbs_to_pysrc([pysrc_base_name] + [path for path, _ in fbss])
  583. unit.onsrcs(['GLOBAL', '{}.py3.fbs.pysrc'.format(pysrc_base_name)])
  584. else:
  585. unit.onfbs_to_py2src([pysrc_base_name] + [path for path, _ in fbss])
  586. unit.onsrcs(['GLOBAL', '{}.py2.fbs.pysrc'.format(pysrc_base_name)])
  587. def _check_test_srcs(*args):
  588. used = set(args) & {"NAMESPACE", "TOP_LEVEL", "__main__.py"}
  589. if used:
  590. param = list(used)[0]
  591. ymake.report_configure_error(
  592. 'in TEST_SRCS: you cannot use {} here - it would broke testing machinery'.format(param)
  593. )
  594. def ontest_srcs(unit, *args):
  595. _check_test_srcs(*args)
  596. if unit.get('PY3TEST_BIN' if is_py3(unit) else 'PYTEST_BIN') != 'no':
  597. unit.onpy_srcs(["NAMESPACE", "__tests__"] + list(args))
  598. def onpy_doctests(unit, *args):
  599. """
  600. @usage PY_DOCTESTS(Packages...)
  601. Add to the test doctests for specified Python packages
  602. The packages should be part of a test (listed as sources of the test or its PEERDIRs).
  603. """
  604. if unit.get('PY3TEST_BIN' if is_py3(unit) else 'PYTEST_BIN') != 'no':
  605. unit.onresource(['-', 'PY_DOCTEST_PACKAGES="{}"'.format(' '.join(args))])
  606. def py_register(unit, func, py3):
  607. if py3:
  608. unit.on_py3_register([func])
  609. else:
  610. unit.on_py_register([func])
  611. def onpy_register(unit, *args):
  612. """
  613. @usage: PY_REGISTER([package.]module_name)
  614. Python knows about which built-ins can be imported, due to their registration in the Assembly or at the start of the interpreter.
  615. All modules from the sources listed in PY_SRCS() are registered automatically.
  616. To register the modules from the sources in the SRCS(), you need to use PY_REGISTER().
  617. PY_REGISTER(module_name) initializes module globally via call to initmodule_name()
  618. PY_REGISTER(package.module_name) initializes module in the specified package
  619. It renames its init function with CFLAGS(-Dinitmodule_name=init7package11module_name)
  620. or CFLAGS(-DPyInit_module_name=PyInit_7package11module_name)
  621. Documentation: https://wiki.yandex-team.ru/arcadia/python/pysrcs/#makrospyregister
  622. """
  623. py3 = is_py3(unit)
  624. for name in args:
  625. assert '=' not in name, name
  626. py_register(unit, name, py3)
  627. if '.' in name:
  628. shortname = name.rsplit('.', 1)[1]
  629. if py3:
  630. unit.oncflags(['-DPyInit_{}=PyInit_{}'.format(shortname, mangle(name))])
  631. else:
  632. unit.oncflags(['-Dinit{}=init{}'.format(shortname, mangle(name))])
  633. def py_main(unit, arg):
  634. if unit.get('IGNORE_PY_MAIN'):
  635. return
  636. unit_needs_main = unit.get('MODULE_TYPE') in ('PROGRAM', 'DLL')
  637. if unit_needs_main:
  638. py_program(unit, is_py3(unit))
  639. unit.onresource(['-', 'PY_MAIN={}'.format(arg)])
  640. def onpy_main(unit, arg):
  641. """
  642. @usage: PY_MAIN(package.module[:func])
  643. Specifies the module or function from which to start executing a python program
  644. Documentation: https://wiki.yandex-team.ru/arcadia/python/pysrcs/#modulipyprogrampy3programimakrospymain
  645. """
  646. arg = arg.replace('/', '.')
  647. if ':' not in arg:
  648. arg += ':main'
  649. py_main(unit, arg)
  650. def onpy_constructor(unit, arg):
  651. """
  652. @usage: PY_CONSTRUCTOR(package.module[:func])
  653. Specifies the module or function which will be started before python's main()
  654. init() is expected in the target module if no function is specified
  655. Can be considered as __attribute__((constructor)) for python
  656. """
  657. if ':' not in arg:
  658. arg = arg + '=init'
  659. else:
  660. arg[arg.index(':')] = '='
  661. unit.onresource(['-', 'py/constructors/{}'.format(arg)])
  662. def onpy_enums_serialization(unit, *args):
  663. ns = ''
  664. args = iter(args)
  665. for arg in args:
  666. # Namespace directives.
  667. if arg == 'NAMESPACE':
  668. ns = next(args)
  669. else:
  670. unit.on_py_enum_serialization_to_json(arg)
  671. unit.on_py_enum_serialization_to_py(arg)
  672. filename = arg.rsplit('.', 1)[0] + '.py'
  673. if len(ns) != 0:
  674. onpy_srcs(unit, 'NAMESPACE', ns, filename)
  675. else:
  676. onpy_srcs(unit, filename)
  677. def oncpp_enums_serialization(unit, *args):
  678. args = iter(args)
  679. for arg in args:
  680. # Namespace directives.
  681. if arg == 'NAMESPACE':
  682. next(args)
  683. else:
  684. unit.ongenerate_enum_serialization_with_header(arg)