/src/tox/package/builder/isolated.py

https://github.com/tox-dev/tox · Python · 149 lines · 114 code · 30 blank · 5 comment · 28 complexity · 84d0362d54914b25d6c0433276196692 MD5 · raw file

  1. from __future__ import unicode_literals
  2. import json
  3. import os
  4. from collections import namedtuple
  5. import six
  6. from packaging.requirements import Requirement
  7. from packaging.utils import canonicalize_name
  8. from tox import reporter
  9. from tox.config import DepConfig, get_py_project_toml
  10. from tox.constants import BUILD_ISOLATED, BUILD_REQUIRE_SCRIPT
  11. BuildInfo = namedtuple(
  12. "BuildInfo",
  13. ["requires", "backend_module", "backend_object", "backend_paths"],
  14. )
  15. def build(config, session):
  16. build_info = get_build_info(config.setupdir)
  17. package_venv = session.getvenv(config.isolated_build_env)
  18. package_venv.envconfig.deps_matches_subset = True
  19. # we allow user specified dependencies so the users can write extensions to
  20. # install additional type of dependencies (e.g. binary)
  21. user_specified_deps = package_venv.envconfig.deps
  22. package_venv.envconfig.deps = [DepConfig(r, None) for r in build_info.requires]
  23. package_venv.envconfig.deps.extend(user_specified_deps)
  24. if package_venv.setupenv():
  25. package_venv.finishvenv()
  26. if isinstance(package_venv.status, Exception):
  27. raise package_venv.status
  28. build_requires = get_build_requires(build_info, package_venv, config.setupdir)
  29. # we need to filter out requirements already specified in pyproject.toml or user deps
  30. base_build_deps = {
  31. canonicalize_name(Requirement(r.name).name) for r in package_venv.envconfig.deps
  32. }
  33. build_requires_dep = [
  34. DepConfig(r, None)
  35. for r in build_requires
  36. if canonicalize_name(Requirement(r).name) not in base_build_deps
  37. ]
  38. if build_requires_dep:
  39. with package_venv.new_action("build_requires", package_venv.envconfig.envdir) as action:
  40. package_venv.run_install_command(packages=build_requires_dep, action=action)
  41. package_venv.finishvenv()
  42. return perform_isolated_build(build_info, package_venv, config.distdir, config.setupdir)
  43. def get_build_info(folder):
  44. toml_file = folder.join("pyproject.toml")
  45. # as per https://www.python.org/dev/peps/pep-0517/
  46. def abort(message):
  47. reporter.error("{} inside {}".format(message, toml_file))
  48. raise SystemExit(1)
  49. if not toml_file.exists():
  50. reporter.error("missing {}".format(toml_file))
  51. raise SystemExit(1)
  52. config_data = get_py_project_toml(toml_file)
  53. if "build-system" not in config_data:
  54. abort("build-system section missing")
  55. build_system = config_data["build-system"]
  56. if "requires" not in build_system:
  57. abort("missing requires key at build-system section")
  58. if "build-backend" not in build_system:
  59. abort("missing build-backend key at build-system section")
  60. requires = build_system["requires"]
  61. if not isinstance(requires, list) or not all(isinstance(i, six.text_type) for i in requires):
  62. abort("requires key at build-system section must be a list of string")
  63. backend = build_system["build-backend"]
  64. if not isinstance(backend, six.text_type):
  65. abort("build-backend key at build-system section must be a string")
  66. args = backend.split(":")
  67. module = args[0]
  68. obj = args[1] if len(args) > 1 else ""
  69. backend_paths = build_system.get("backend-path", [])
  70. if not isinstance(backend_paths, list):
  71. abort("backend-path key at build-system section must be a list, if specified")
  72. backend_paths = [folder.join(p) for p in backend_paths]
  73. normalized_folder = os.path.normcase(str(folder.realpath()))
  74. normalized_paths = (os.path.normcase(str(path.realpath())) for path in backend_paths)
  75. if not all(
  76. os.path.commonprefix((normalized_folder, path)) == normalized_folder
  77. for path in normalized_paths
  78. ):
  79. abort("backend-path must exist in the project root")
  80. return BuildInfo(requires, module, obj, backend_paths)
  81. def perform_isolated_build(build_info, package_venv, dist_dir, setup_dir):
  82. with package_venv.new_action(
  83. "perform-isolated-build",
  84. package_venv.envconfig.envdir,
  85. ) as action:
  86. # need to start with an empty (but existing) source distribution folder
  87. if dist_dir.exists():
  88. dist_dir.remove(rec=1, ignore_errors=True)
  89. dist_dir.ensure_dir()
  90. result = package_venv._pcall(
  91. [
  92. package_venv.envconfig.envpython,
  93. BUILD_ISOLATED,
  94. str(dist_dir),
  95. build_info.backend_module,
  96. build_info.backend_object,
  97. os.path.pathsep.join(str(p) for p in build_info.backend_paths),
  98. ],
  99. returnout=True,
  100. action=action,
  101. cwd=setup_dir,
  102. )
  103. reporter.verbosity2(result)
  104. return dist_dir.join(result.split("\n")[-2])
  105. def get_build_requires(build_info, package_venv, setup_dir):
  106. with package_venv.new_action("get-build-requires", package_venv.envconfig.envdir) as action:
  107. result = package_venv._pcall(
  108. [
  109. package_venv.envconfig.envpython,
  110. BUILD_REQUIRE_SCRIPT,
  111. build_info.backend_module,
  112. build_info.backend_object,
  113. os.path.pathsep.join(str(p) for p in build_info.backend_paths),
  114. ],
  115. returnout=True,
  116. action=action,
  117. cwd=setup_dir,
  118. )
  119. return json.loads(result.split("\n")[-2])