nots.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754
  1. import os
  2. import ymake
  3. import ytest
  4. from _common import resolve_common_const, get_norm_unit_path, rootrel_arc_src, to_yesno
  5. # 1 is 60 files per chunk for TIMEOUT(60) - default timeout for SIZE(SMALL)
  6. # 0.5 is 120 files per chunk for TIMEOUT(60) - default timeout for SIZE(SMALL)
  7. # 0.2 is 300 files per chunk for TIMEOUT(60) - default timeout for SIZE(SMALL)
  8. ESLINT_FILE_PROCESSING_TIME_DEFAULT = 0.2 # seconds per file
  9. class PluginLogger(object):
  10. def __init__(self):
  11. self.unit = None
  12. self.prefix = ""
  13. def reset(self, unit, prefix=""):
  14. self.unit = unit
  15. self.prefix = prefix
  16. def get_state(self):
  17. return (self.unit, self.prefix)
  18. def _stringify_messages(self, messages):
  19. parts = []
  20. for m in messages:
  21. if m is None:
  22. parts.append("None")
  23. else:
  24. parts.append(m if isinstance(m, str) else repr(m))
  25. # cyan color (code 36) for messages
  26. return "\033[0;32m{}\033[0;49m\n\033[0;36m{}\033[0;49m".format(self.prefix, " ".join(parts))
  27. def info(self, *messages):
  28. if self.unit:
  29. self.unit.message(["INFO", self._stringify_messages(messages)])
  30. def warn(self, *messages):
  31. if self.unit:
  32. self.unit.message(["WARN", self._stringify_messages(messages)])
  33. def error(self, *messages):
  34. if self.unit:
  35. self.unit.message(["ERROR", self._stringify_messages(messages)])
  36. def print_vars(self, *variables):
  37. if self.unit:
  38. values = ["{}={}".format(v, self.unit.get(v)) for v in variables]
  39. self.info("\n".join(values))
  40. logger = PluginLogger()
  41. def _with_report_configure_error(fn):
  42. def _wrapper(*args, **kwargs):
  43. last_state = logger.get_state()
  44. unit = args[0]
  45. logger.reset(unit if unit.get("TS_LOG") == "yes" else None, fn.__name__)
  46. try:
  47. fn(*args, **kwargs)
  48. except Exception as exc:
  49. ymake.report_configure_error(str(exc))
  50. if unit.get("TS_RAISE") == "yes":
  51. raise
  52. else:
  53. unit.message(["WARN", "Configure error is reported. Add -DTS_RAISE to see actual exception"])
  54. finally:
  55. logger.reset(*last_state)
  56. return _wrapper
  57. def _build_directives(name, flags, paths):
  58. # type: (str, list[str]|tuple[str], list[str]) -> str
  59. parts = [p for p in [name] + (flags or []) if p]
  60. parts_str = ";".join(parts)
  61. expressions = ['${{{parts}:"{path}"}}'.format(parts=parts_str, path=path) for path in paths]
  62. return " ".join(expressions)
  63. def _build_cmd_input_paths(paths, hide=False, disable_include_processor=False):
  64. # type: (list[str]|tuple[str], bool, bool) -> str
  65. hide_part = "hide" if hide else ""
  66. disable_ip_part = "context=TEXT" if disable_include_processor else ""
  67. return _build_directives("input", [hide_part, disable_ip_part], paths)
  68. def _create_pm(unit):
  69. from lib.nots.package_manager import manager
  70. sources_path = unit.path()
  71. module_path = unit.get("MODDIR")
  72. if unit.get("TS_TEST_FOR"):
  73. sources_path = unit.get("TS_TEST_FOR_DIR")
  74. module_path = unit.get("TS_TEST_FOR_PATH")
  75. return manager(
  76. sources_path=unit.resolve(sources_path),
  77. build_root="$B",
  78. build_path=unit.path().replace("$S", "$B", 1),
  79. contribs_path=unit.get("NPM_CONTRIBS_PATH"),
  80. nodejs_bin_path=None,
  81. script_path=None,
  82. module_path=module_path,
  83. )
  84. def _create_erm_json(unit):
  85. from lib.nots.erm_json_lite import ErmJsonLite
  86. erm_packages_path = unit.get("ERM_PACKAGES_PATH")
  87. path = unit.resolve(unit.resolve_arc_path(erm_packages_path))
  88. return ErmJsonLite.load(path)
  89. @_with_report_configure_error
  90. def on_set_append_with_directive(unit, var_name, dir, *values):
  91. wrapped = ['${{{dir}:"{v}"}}'.format(dir=dir, v=v) for v in values]
  92. __set_append(unit, var_name, " ".join(wrapped))
  93. @_with_report_configure_error
  94. def on_from_npm_lockfiles(unit, *args):
  95. from lib.nots.package_manager.base import PackageManagerError
  96. pm = _create_pm(unit)
  97. lf_paths = []
  98. for lf_path in args:
  99. abs_lf_path = unit.resolve(unit.resolve_arc_path(lf_path))
  100. if abs_lf_path:
  101. lf_paths.append(abs_lf_path)
  102. elif unit.get("TS_STRICT_FROM_NPM_LOCKFILES") == "yes":
  103. ymake.report_configure_error("lockfile not found: {}".format(lf_path))
  104. try:
  105. for pkg in pm.extract_packages_meta_from_lockfiles(lf_paths):
  106. unit.on_from_npm([pkg.tarball_url, pkg.sky_id, pkg.integrity, pkg.integrity_algorithm, pkg.tarball_path])
  107. except PackageManagerError as e:
  108. logger.warn(str(e))
  109. pass
  110. def _check_nodejs_version(unit, major):
  111. if major < 14:
  112. raise Exception(
  113. "Node.js {} is unsupported. Update Node.js please. See https://nda.ya.ru/t/joB9Mivm6h4znu".format(major)
  114. )
  115. if major < 18:
  116. unit.message(
  117. [
  118. "WARN",
  119. "Node.js {} is deprecated. Update Node.js please. See https://nda.ya.ru/t/joB9Mivm6h4znu".format(major),
  120. ]
  121. )
  122. @_with_report_configure_error
  123. def on_peerdir_ts_resource(unit, *resources):
  124. pm = _create_pm(unit)
  125. pj = pm.load_package_json_from_dir(pm.sources_path)
  126. erm_json = _create_erm_json(unit)
  127. dirs = []
  128. nodejs_version = _select_matching_version(erm_json, "nodejs", pj.get_nodejs_version())
  129. _check_nodejs_version(unit, nodejs_version.major)
  130. for tool in resources:
  131. dir_name = erm_json.canonize_name(tool)
  132. if erm_json.use_resource_directly(tool):
  133. # raises the configuration error when the version is unsupported
  134. _select_matching_version(erm_json, tool, pj.get_dep_specifier(tool), dep_is_required=True)
  135. elif tool == "nodejs":
  136. dirs.append(os.path.join("build", "platform", dir_name, str(nodejs_version)))
  137. _set_resource_vars(unit, erm_json, tool, nodejs_version)
  138. elif erm_json.is_resource_multiplatform(tool):
  139. v = _select_matching_version(erm_json, tool, pj.get_dep_specifier(tool))
  140. sb_resources = [
  141. sbr for sbr in erm_json.get_sb_resources(tool, v) if sbr.get("nodejs") == nodejs_version.major
  142. ]
  143. nodejs_dir = "NODEJS_{}".format(nodejs_version.major)
  144. if len(sb_resources) > 0:
  145. dirs.append(os.path.join("build", "external_resources", dir_name, str(v), nodejs_dir))
  146. _set_resource_vars(unit, erm_json, tool, v, nodejs_version.major)
  147. else:
  148. unit.message(["WARN", "Missing {}@{} for {}".format(tool, str(v), nodejs_dir)])
  149. else:
  150. v = _select_matching_version(erm_json, tool, pj.get_dep_specifier(tool))
  151. dirs.append(os.path.join("build", "external_resources", dir_name, str(v)))
  152. _set_resource_vars(unit, erm_json, tool, v, nodejs_version.major)
  153. if dirs:
  154. unit.onpeerdir(dirs)
  155. @_with_report_configure_error
  156. def on_ts_configure(unit):
  157. # type: (Unit) -> None
  158. from lib.nots.package_manager.base import PackageJson
  159. from lib.nots.package_manager.base.utils import build_pj_path
  160. from lib.nots.typescript import TsConfig
  161. tsconfig_paths = unit.get("TS_CONFIG_PATH").split()
  162. # for use in CMD as inputs
  163. __set_append(
  164. unit, "TS_CONFIG_FILES", _build_cmd_input_paths(tsconfig_paths, hide=True, disable_include_processor=True)
  165. )
  166. mod_dir = unit.get("MODDIR")
  167. cur_dir = unit.get("TS_TEST_FOR_PATH") if unit.get("TS_TEST_FOR") else mod_dir
  168. pj_path = build_pj_path(unit.resolve(unit.resolve_arc_path(cur_dir)))
  169. dep_paths = PackageJson.load(pj_path).get_dep_paths_by_names()
  170. # reversed for using the first tsconfig as the config for include processor (legacy)
  171. for tsconfig_path in reversed(tsconfig_paths):
  172. abs_tsconfig_path = unit.resolve(unit.resolve_arc_path(tsconfig_path))
  173. if not abs_tsconfig_path:
  174. raise Exception("tsconfig not found: {}".format(tsconfig_path))
  175. tsconfig = TsConfig.load(abs_tsconfig_path)
  176. config_files = tsconfig.inline_extend(dep_paths)
  177. config_files = _resolve_module_files(unit, mod_dir, config_files)
  178. use_tsconfig_outdir = unit.get("TS_CONFIG_USE_OUTDIR") == "yes"
  179. tsconfig.validate(use_tsconfig_outdir)
  180. # add tsconfig files from which root tsconfig files were extended
  181. __set_append(
  182. unit, "TS_CONFIG_FILES", _build_cmd_input_paths(config_files, hide=True, disable_include_processor=True)
  183. )
  184. # region include processor
  185. unit.set(["TS_CONFIG_ROOT_DIR", tsconfig.compiler_option("rootDir")]) # also for hermione
  186. if use_tsconfig_outdir:
  187. unit.set(["TS_CONFIG_OUT_DIR", tsconfig.compiler_option("outDir")]) # also for hermione
  188. unit.set(["TS_CONFIG_SOURCE_MAP", to_yesno(tsconfig.compiler_option("sourceMap"))])
  189. unit.set(["TS_CONFIG_DECLARATION", to_yesno(tsconfig.compiler_option("declaration"))])
  190. unit.set(["TS_CONFIG_DECLARATION_MAP", to_yesno(tsconfig.compiler_option("declarationMap"))])
  191. unit.set(["TS_CONFIG_PRESERVE_JSX", to_yesno(tsconfig.compiler_option("jsx") == "preserve")])
  192. # endregion
  193. _filter_inputs_by_rules_from_tsconfig(unit, tsconfig)
  194. _setup_eslint(unit)
  195. _setup_tsc_typecheck(unit, tsconfig_paths)
  196. @_with_report_configure_error
  197. def on_setup_build_env(unit): # type: (Unit) -> None
  198. build_env_var = unit.get("TS_BUILD_ENV") # type: str
  199. if not build_env_var:
  200. return
  201. options = []
  202. for name in build_env_var.split(","):
  203. options.append("--env")
  204. value = unit.get(f"TS_ENV_{name}")
  205. if value is None:
  206. ymake.report_configure_error(f"Env var '{name}' is provided in a list, but var value is not provided")
  207. continue
  208. double_quote_escaped_value = value.replace('"', '\\"')
  209. options.append(f'"{name}={double_quote_escaped_value}"')
  210. unit.set(["NOTS_TOOL_BUILD_ENV", " ".join(options)])
  211. def __set_append(unit, var_name, value):
  212. # type: (Unit, str, str|list[str]|tuple[str]) -> None
  213. """
  214. SET_APPEND() python naive implementation - append value/values to the list of values
  215. """
  216. previous_value = unit.get(var_name) or ""
  217. value_in_str = " ".join(value) if isinstance(value, list) or isinstance(value, tuple) else value
  218. new_value = previous_value + " " + value_in_str
  219. unit.set([var_name, new_value])
  220. def __strip_prefix(prefix, line):
  221. # type: (str, str) -> str
  222. if line.startswith(prefix):
  223. prefix_len = len(prefix)
  224. return line[prefix_len:]
  225. return line
  226. def _filter_inputs_by_rules_from_tsconfig(unit, tsconfig):
  227. """
  228. Reduce file list from the TS_GLOB_FILES variable following tsconfig.json rules
  229. """
  230. mod_dir = unit.get("MODDIR")
  231. target_path = os.path.join("${ARCADIA_ROOT}", mod_dir, "") # To have "/" in the end
  232. all_files = [__strip_prefix(target_path, f) for f in unit.get("TS_GLOB_FILES").split(" ")]
  233. filtered_files = tsconfig.filter_files(all_files)
  234. __set_append(unit, "TS_INPUT_FILES", [os.path.join(target_path, f) for f in filtered_files])
  235. def _get_ts_test_data_dirs(unit):
  236. return sorted(
  237. set(
  238. [
  239. os.path.dirname(rootrel_arc_src(p, unit))
  240. for p in (ytest.get_values_list(unit, "_TS_TEST_DATA_VALUE") or [])
  241. ]
  242. )
  243. )
  244. def _resolve_config_path(unit, test_runner, rel_to):
  245. config_path = unit.get("ESLINT_CONFIG_PATH") if test_runner == "eslint" else unit.get("TS_TEST_CONFIG_PATH")
  246. arc_config_path = unit.resolve_arc_path(config_path)
  247. abs_config_path = unit.resolve(arc_config_path)
  248. if not abs_config_path:
  249. raise Exception("{} config not found: {}".format(test_runner, config_path))
  250. unit.onsrcs([arc_config_path])
  251. abs_rel_to = unit.resolve(unit.resolve_arc_path(unit.get(rel_to)))
  252. return os.path.relpath(abs_config_path, start=abs_rel_to)
  253. def _is_tests_enabled(unit):
  254. if unit.get("TIDY") == "yes":
  255. return False
  256. return True
  257. def _get_test_runner_handlers():
  258. return {
  259. "jest": _add_jest_ts_test,
  260. "hermione": _add_hermione_ts_test,
  261. "playwright": _add_playwright_ts_test,
  262. }
  263. def _add_jest_ts_test(unit, test_runner, test_files, deps, test_record):
  264. test_record.update(
  265. {
  266. "CONFIG-PATH": _resolve_config_path(unit, test_runner, rel_to="TS_TEST_FOR_PATH"),
  267. }
  268. )
  269. _add_test(unit, test_runner, test_files, deps, test_record)
  270. def _add_hermione_ts_test(unit, test_runner, test_files, deps, test_record):
  271. test_tags = sorted(set(["ya:fat", "ya:external", "ya:noretries"] + ytest.get_values_list(unit, "TEST_TAGS_VALUE")))
  272. test_requirements = sorted(set(["network:full"] + ytest.get_values_list(unit, "TEST_REQUIREMENTS_VALUE")))
  273. test_record.update(
  274. {
  275. "SIZE": "LARGE",
  276. "TAG": ytest.serialize_list(test_tags),
  277. "REQUIREMENTS": ytest.serialize_list(test_requirements),
  278. "CONFIG-PATH": _resolve_config_path(unit, test_runner, rel_to="TS_TEST_FOR_PATH"),
  279. }
  280. )
  281. _add_test(unit, test_runner, test_files, deps, test_record)
  282. def _add_playwright_ts_test(unit, test_runner, test_files, deps, test_record):
  283. test_record.update(
  284. {
  285. "CONFIG-PATH": _resolve_config_path(unit, test_runner, rel_to="TS_TEST_FOR_PATH"),
  286. }
  287. )
  288. _add_test(unit, test_runner, test_files, deps, test_record)
  289. def _setup_eslint(unit):
  290. if not _is_tests_enabled(unit):
  291. return
  292. if unit.get("_NO_LINT_VALUE") == "none":
  293. return
  294. lint_files = ytest.get_values_list(unit, "_TS_LINT_SRCS_VALUE")
  295. if not lint_files:
  296. return
  297. mod_dir = unit.get("MODDIR")
  298. unit.on_peerdir_ts_resource("eslint")
  299. user_recipes = unit.get("TEST_RECIPES_VALUE")
  300. unit.on_setup_install_node_modules_recipe()
  301. lint_files = _resolve_module_files(unit, mod_dir, lint_files)
  302. deps = _create_pm(unit).get_peers_from_package_json()
  303. test_record = {
  304. "ESLINT_CONFIG_PATH": _resolve_config_path(unit, "eslint", rel_to="MODDIR"),
  305. "LINT-FILE-PROCESSING-TIME": str(ESLINT_FILE_PROCESSING_TIME_DEFAULT),
  306. }
  307. _add_test(unit, "eslint", lint_files, deps, test_record, mod_dir)
  308. unit.set(["TEST_RECIPES_VALUE", user_recipes])
  309. def _setup_tsc_typecheck(unit, tsconfig_paths: list[str]):
  310. if not _is_tests_enabled(unit):
  311. return
  312. if unit.get("_TS_TYPECHECK_VALUE") == "none":
  313. return
  314. typecheck_files = ytest.get_values_list(unit, "TS_INPUT_FILES")
  315. if not typecheck_files:
  316. return
  317. tsconfig_path = tsconfig_paths[0]
  318. if len(tsconfig_paths) > 1:
  319. tsconfig_path = unit.get("_TS_TYPECHECK_TSCONFIG")
  320. if not tsconfig_path:
  321. macros = " or ".join([f"TS_TYPECHECK({p})" for p in tsconfig_paths])
  322. raise Exception(f"Module uses several tsconfig files, specify which one to use for typecheck: {macros}")
  323. abs_tsconfig_path = unit.resolve(unit.resolve_arc_path(tsconfig_path))
  324. if not abs_tsconfig_path:
  325. raise Exception(f"tsconfig for typecheck not found: {tsconfig_path}")
  326. unit.on_peerdir_ts_resource("typescript")
  327. user_recipes = unit.get("TEST_RECIPES_VALUE")
  328. unit.on_setup_install_node_modules_recipe()
  329. unit.on_setup_extract_output_tars_recipe([unit.get("MODDIR")])
  330. _add_test(
  331. unit,
  332. test_type="tsc_typecheck",
  333. test_files=[resolve_common_const(f) for f in typecheck_files],
  334. deps=_create_pm(unit).get_peers_from_package_json(),
  335. test_record={"TS_CONFIG_PATH": tsconfig_path},
  336. test_cwd=unit.get("MODDIR"),
  337. )
  338. unit.set(["TEST_RECIPES_VALUE", user_recipes])
  339. def _resolve_module_files(unit, mod_dir, file_paths):
  340. mod_dir_with_sep_len = len(mod_dir) + 1
  341. resolved_files = []
  342. for path in file_paths:
  343. resolved = rootrel_arc_src(path, unit)
  344. if resolved.startswith(mod_dir):
  345. resolved = resolved[mod_dir_with_sep_len:]
  346. resolved_files.append(resolved)
  347. return resolved_files
  348. def _add_test(unit, test_type, test_files, deps=None, test_record=None, test_cwd=None):
  349. from lib.nots.package_manager import constants
  350. def sort_uniq(text):
  351. return sorted(set(text))
  352. recipes_lines = ytest.format_recipes(unit.get("TEST_RECIPES_VALUE")).strip().splitlines()
  353. if recipes_lines:
  354. deps = deps or []
  355. deps.extend([os.path.dirname(r.strip().split(" ")[0]) for r in recipes_lines])
  356. if deps:
  357. joined_deps = "\n".join(deps)
  358. logger.info(f"{test_type} deps: \n{joined_deps}")
  359. unit.ondepends(deps)
  360. test_dir = get_norm_unit_path(unit)
  361. full_test_record = {
  362. # Key to discover suite (see devtools/ya/test/explore/__init__.py#gen_suite)
  363. "SCRIPT-REL-PATH": test_type,
  364. # Test name as shown in PR check, should be unique inside one module
  365. "TEST-NAME": test_type.lower().replace(".new", ""),
  366. "TEST-TIMEOUT": unit.get("TEST_TIMEOUT") or "",
  367. "TEST-ENV": ytest.prepare_env(unit.get("TEST_ENV_VALUE")),
  368. "TESTED-PROJECT-NAME": os.path.splitext(unit.filename())[0],
  369. "TEST-RECIPES": ytest.prepare_recipes(unit.get("TEST_RECIPES_VALUE")),
  370. "SOURCE-FOLDER-PATH": test_dir,
  371. "BUILD-FOLDER-PATH": test_dir,
  372. "BINARY-PATH": os.path.join(test_dir, unit.filename()),
  373. "SPLIT-FACTOR": unit.get("TEST_SPLIT_FACTOR") or "",
  374. "FORK-MODE": unit.get("TEST_FORK_MODE") or "",
  375. "SIZE": unit.get("TEST_SIZE_NAME") or "",
  376. "TEST-DATA": ytest.serialize_list(ytest.get_values_list(unit, "TEST_DATA_VALUE")),
  377. "TEST-FILES": ytest.serialize_list(test_files),
  378. "TEST-CWD": test_cwd or "",
  379. "TAG": ytest.serialize_list(ytest.get_values_list(unit, "TEST_TAGS_VALUE")),
  380. "REQUIREMENTS": ytest.serialize_list(ytest.get_values_list(unit, "TEST_REQUIREMENTS_VALUE")),
  381. "NODEJS-ROOT-VAR-NAME": unit.get("NODEJS-ROOT-VAR-NAME"),
  382. "NODE-MODULES-BUNDLE-FILENAME": constants.NODE_MODULES_WORKSPACE_BUNDLE_FILENAME,
  383. "CUSTOM-DEPENDENCIES": " ".join(sort_uniq((deps or []) + ytest.get_values_list(unit, "TEST_DEPENDS_VALUE"))),
  384. }
  385. if test_record:
  386. full_test_record.update(test_record)
  387. data = ytest.dump_test(unit, full_test_record)
  388. if data:
  389. unit.set_property(["DART_DATA", data])
  390. def _set_resource_vars(unit, erm_json, tool, version, nodejs_major=None):
  391. # type: (any, ErmJsonLite, Version, str|None, int|None) -> None
  392. resource_name = erm_json.canonize_name(tool).upper()
  393. # example: NODEJS_12_18_4 | HERMIONE_7_0_4_NODEJS_18
  394. version_str = str(version).replace(".", "_")
  395. yamake_resource_name = "{}_{}".format(resource_name, version_str)
  396. if erm_json.is_resource_multiplatform(tool):
  397. yamake_resource_name += "_NODEJS_{}".format(nodejs_major)
  398. yamake_resource_var = "{}_RESOURCE_GLOBAL".format(yamake_resource_name)
  399. unit.set(["{}_ROOT".format(resource_name), "${}".format(yamake_resource_var)])
  400. unit.set(["{}-ROOT-VAR-NAME".format(resource_name), yamake_resource_var])
  401. def _select_matching_version(erm_json, resource_name, range_str, dep_is_required=False):
  402. # type: (ErmJsonLite, str, str, bool) -> Version
  403. if dep_is_required and range_str is None:
  404. raise Exception(
  405. "Please install the '{tool}' package to the project. Run the command:\n"
  406. " ya tool nots add -D {tool}".format(tool=resource_name)
  407. )
  408. try:
  409. version = erm_json.select_version_of(resource_name, range_str)
  410. if version:
  411. return version
  412. raise ValueError("There is no allowed version to satisfy this range: '{}'".format(range_str))
  413. except Exception as error:
  414. toolchain_versions = erm_json.get_versions_of(erm_json.get_resource(resource_name))
  415. raise Exception(
  416. "Requested {} version range '{}' could not be satisfied. \n"
  417. "Please use a range that would include one of the following: {}. \n"
  418. "For further details please visit the link: {} \nOriginal error: {} \n".format(
  419. resource_name,
  420. range_str,
  421. ", ".join(map(str, toolchain_versions)),
  422. "https://docs.yandex-team.ru/frontend-in-arcadia/_generated/toolchain",
  423. str(error),
  424. )
  425. )
  426. @_with_report_configure_error
  427. def on_prepare_deps_configure(unit):
  428. contrib_path = unit.get("NPM_CONTRIBS_PATH")
  429. if contrib_path == '-':
  430. unit.on_prepare_deps_configure_no_contrib()
  431. return
  432. unit.onpeerdir(contrib_path)
  433. pm = _create_pm(unit)
  434. pj = pm.load_package_json_from_dir(pm.sources_path)
  435. has_deps = pj.has_dependencies()
  436. ins, outs = pm.calc_prepare_deps_inouts(unit.get("_TARBALLS_STORE"), has_deps)
  437. if has_deps:
  438. unit.onpeerdir(pm.get_local_peers_from_package_json())
  439. __set_append(unit, "_PREPARE_DEPS_INOUTS", _build_directives("input", ["hide"], sorted(ins)))
  440. __set_append(unit, "_PREPARE_DEPS_INOUTS", _build_directives("output", ["hide"], sorted(outs)))
  441. else:
  442. __set_append(unit, "_PREPARE_DEPS_INOUTS", _build_directives("output", [], sorted(outs)))
  443. unit.set(["_PREPARE_DEPS_CMD", "$_PREPARE_NO_DEPS_CMD"])
  444. @_with_report_configure_error
  445. def on_prepare_deps_configure_no_contrib(unit):
  446. pm = _create_pm(unit)
  447. pj = pm.load_package_json_from_dir(pm.sources_path)
  448. has_deps = pj.has_dependencies()
  449. ins, outs, resources = pm.calc_prepare_deps_inouts_and_resources(unit.get("_TARBALLS_STORE"), has_deps)
  450. if has_deps:
  451. unit.onpeerdir(pm.get_local_peers_from_package_json())
  452. __set_append(unit, "_PREPARE_DEPS_INOUTS", _build_directives("input", ["hide"], sorted(ins)))
  453. __set_append(unit, "_PREPARE_DEPS_INOUTS", _build_directives("output", ["hide"], sorted(outs)))
  454. unit.set(["_PREPARE_DEPS_RESOURCES", " ".join([f'${{resource:"{uri}"}}' for uri in sorted(resources)])])
  455. unit.set(["_PREPARE_DEPS_USE_RESOURCES_FLAG", "--resource-root $(RESOURCE_ROOT)"])
  456. else:
  457. __set_append(unit, "_PREPARE_DEPS_INOUTS", _build_directives("output", [], sorted(outs)))
  458. unit.set(["_PREPARE_DEPS_CMD", "$_PREPARE_NO_DEPS_CMD"])
  459. @_with_report_configure_error
  460. def on_node_modules_configure(unit):
  461. pm = _create_pm(unit)
  462. pj = pm.load_package_json_from_dir(pm.sources_path)
  463. if pj.has_dependencies():
  464. unit.onpeerdir(pm.get_local_peers_from_package_json())
  465. local_cli = unit.get("TS_LOCAL_CLI") == "yes"
  466. ins, outs = pm.calc_node_modules_inouts(local_cli)
  467. __set_append(unit, "_NODE_MODULES_INOUTS", _build_directives("input", ["hide"], sorted(ins)))
  468. if not unit.get("TS_TEST_FOR"):
  469. __set_append(unit, "_NODE_MODULES_INOUTS", _build_directives("output", ["hide"], sorted(outs)))
  470. if pj.get_use_prebuilder():
  471. unit.on_peerdir_ts_resource("@yatool/prebuilder")
  472. unit.set(
  473. [
  474. "_YATOOL_PREBUILDER_ARG",
  475. "--yatool-prebuilder-path $YATOOL_PREBUILDER_ROOT/node_modules/@yatool/prebuilder",
  476. ]
  477. )
  478. # YATOOL_PREBUILDER_0_7_0_RESOURCE_GLOBAL
  479. prebuilder_major = unit.get("YATOOL_PREBUILDER-ROOT-VAR-NAME").split("_")[2]
  480. logger.info(f"Detected prebuilder \033[0;32mv{prebuilder_major}.x.x\033[0;49m")
  481. if prebuilder_major == "0":
  482. # TODO: FBP-1408
  483. lf = pm.load_lockfile_from_dir(pm.sources_path)
  484. is_valid, invalid_keys = lf.validate_has_addons_flags()
  485. if not is_valid:
  486. ymake.report_configure_error(
  487. "Project is configured to use @yatool/prebuilder. \n"
  488. + "Some packages in the pnpm-lock.yaml are misconfigured.\n"
  489. + "Run \033[0;32m`ya tool nots update-lockfile`\033[0;49m to fix lockfile.\n"
  490. + "All packages with `requiresBuild:true` have to be marked with `hasAddons:true/false`.\n"
  491. + "Misconfigured keys: \n"
  492. + " - "
  493. + "\n - ".join(invalid_keys)
  494. )
  495. else:
  496. lf = pm.load_lockfile_from_dir(pm.sources_path)
  497. requires_build_packages = lf.get_requires_build_packages()
  498. is_valid, validation_messages = pj.validate_prebuilds(requires_build_packages)
  499. if not is_valid:
  500. ymake.report_configure_error(
  501. "Project is configured to use @yatool/prebuilder. \n"
  502. + "Some packages are misconfigured.\n"
  503. + "Run \033[0;32m`ya tool nots update-lockfile`\033[0;49m to fix pnpm-lock.yaml and package.json.\n"
  504. + "Validation details: \n"
  505. + "\n".join(validation_messages)
  506. )
  507. @_with_report_configure_error
  508. def on_ts_test_for_configure(unit, test_runner, default_config, node_modules_filename):
  509. if not _is_tests_enabled(unit):
  510. return
  511. if unit.enabled('TS_COVERAGE'):
  512. unit.on_peerdir_ts_resource("nyc")
  513. for_mod_path = unit.get("TS_TEST_FOR_PATH")
  514. unit.onpeerdir([for_mod_path])
  515. unit.on_setup_extract_node_modules_recipe([for_mod_path])
  516. unit.on_setup_extract_output_tars_recipe([for_mod_path])
  517. root = "$B" if test_runner == "hermione" else "$(BUILD_ROOT)"
  518. unit.set(["TS_TEST_NM", os.path.join(root, for_mod_path, node_modules_filename)])
  519. config_path = unit.get("TS_TEST_CONFIG_PATH")
  520. if not config_path:
  521. config_path = os.path.join(for_mod_path, default_config)
  522. unit.set(["TS_TEST_CONFIG_PATH", config_path])
  523. test_record = _add_ts_resources_to_test_record(
  524. unit,
  525. {
  526. "TS-TEST-FOR-PATH": for_mod_path,
  527. "TS-TEST-DATA-DIRS": ytest.serialize_list(_get_ts_test_data_dirs(unit)),
  528. "TS-TEST-DATA-DIRS-RENAME": unit.get("_TS_TEST_DATA_DIRS_RENAME_VALUE"),
  529. },
  530. )
  531. test_files = ytest.get_values_list(unit, "_TS_TEST_SRCS_VALUE")
  532. test_files = _resolve_module_files(unit, unit.get("MODDIR"), test_files)
  533. if not test_files:
  534. ymake.report_configure_error("No tests found")
  535. return
  536. deps = _create_pm(unit).get_peers_from_package_json()
  537. add_ts_test = _get_test_runner_handlers()[test_runner]
  538. add_ts_test(unit, test_runner, test_files, deps, test_record)
  539. @_with_report_configure_error
  540. def on_validate_ts_test_for_args(unit, for_mod, root):
  541. # FBP-1085
  542. is_arc_root = root == "${ARCADIA_ROOT}"
  543. is_rel_for_mod = for_mod.startswith(".")
  544. if is_arc_root and is_rel_for_mod:
  545. ymake.report_configure_error(
  546. "You are using a relative path for a module. "
  547. + "You have to add RELATIVE key, like (RELATIVE {})".format(for_mod)
  548. )
  549. @_with_report_configure_error
  550. def on_set_ts_test_for_vars(unit, for_mod):
  551. unit.set(["TS_TEST_FOR", "yes"])
  552. unit.set(["TS_TEST_FOR_DIR", unit.resolve_arc_path(for_mod)])
  553. unit.set(["TS_TEST_FOR_PATH", rootrel_arc_src(for_mod, unit)])
  554. def _add_ts_resources_to_test_record(unit, test_record):
  555. erm_json = _create_erm_json(unit)
  556. for tool in erm_json.list_npm_packages():
  557. tool_resource_label = "{}-ROOT-VAR-NAME".format(tool.upper())
  558. tool_resource_value = unit.get(tool_resource_label)
  559. if tool_resource_value:
  560. test_record[tool_resource_label] = tool_resource_value
  561. return test_record
  562. @_with_report_configure_error
  563. def on_ts_files(unit, *files):
  564. new_cmds = ['$COPY_CMD ${{input;context=TEXT:"{0}"}} ${{output;noauto:"{0}"}}'.format(f) for f in files]
  565. all_cmds = unit.get("_TS_FILES_COPY_CMD")
  566. if all_cmds:
  567. new_cmds.insert(0, all_cmds)
  568. unit.set(["_TS_FILES_COPY_CMD", " && ".join(new_cmds)])
  569. @_with_report_configure_error
  570. def on_ts_package_check_files(unit):
  571. ts_files = unit.get("_TS_FILES_COPY_CMD")
  572. if ts_files == "":
  573. ymake.report_configure_error(
  574. "\n"
  575. "In the TS_PACKAGE module, you should define at least one file using the TS_FILES() macro.\n"
  576. "Docs: https://docs.yandex-team.ru/frontend-in-arcadia/references/TS_PACKAGE#ts-files."
  577. )
  578. @_with_report_configure_error
  579. def on_depends_on_mod(unit):
  580. if unit.get("_TS_TEST_DEPENDS_ON_BUILD"):
  581. for_mod_path = unit.get("TS_TEST_FOR_PATH")
  582. unit.ondepends([for_mod_path])