diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index 5d5d77f29f6eb1..56bcc990bd8cf1 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -17,7 +17,7 @@ on: - "Tools/build/check_warnings.py" - "Tools/build/compute-changes.py" - "Tools/build/deepfreeze.py" - - "Tools/build/generate-build-details.py" + - "Tools/build/generate_build_details.py" - "Tools/build/generate_sbom.py" - "Tools/build/generate_stdlib_module_names.py" - "Tools/build/mypy.ini" diff --git a/.gitignore b/.gitignore index e842676d866bf8..e13fa9cdd73065 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,8 @@ gmon.out .pytest_cache/ .ruff_cache/ .DS_Store +__install__.json +build-details.json *.exe diff --git a/Lib/test/test_build_details.py b/Lib/test/test_build_details.py index ba9afe69ba46e8..fc41508460bf12 100644 --- a/Lib/test/test_build_details.py +++ b/Lib/test/test_build_details.py @@ -1,4 +1,3 @@ -import importlib import json import os import os.path @@ -6,27 +5,14 @@ import sysconfig import string import unittest -from pathlib import Path from test.support import is_android, is_apple_mobile, is_wasm32 - -BASE_PATH = Path( - __file__, # Lib/test/test_build_details.py - '..', # Lib/test - '..', # Lib - '..', # -).resolve() -MODULE_PATH = BASE_PATH / 'Tools' / 'build' / 'generate-build-details.py' +from test.test_tools import imports_under_tool try: - # Import "generate-build-details.py" as "generate_build_details" - spec = importlib.util.spec_from_file_location( - "generate_build_details", MODULE_PATH - ) - generate_build_details = importlib.util.module_from_spec(spec) - sys.modules["generate_build_details"] = generate_build_details - spec.loader.exec_module(generate_build_details) -except (FileNotFoundError, ImportError): + with imports_under_tool('build'): + import generate_build_details +except ImportError: generate_build_details = None @@ -116,7 +102,6 @@ def test_implementation(self): ) -@unittest.skipIf(os.name != 'posix', 'Feature only implemented on POSIX right now') @unittest.skipIf(is_wasm32, 'Feature not available on WebAssembly builds') class CPythonBuildDetailsTests(unittest.TestCase, FormatTestsBase): """Test CPython's install details file implementation.""" @@ -124,10 +109,13 @@ class CPythonBuildDetailsTests(unittest.TestCase, FormatTestsBase): @property def location(self): if sysconfig.is_python_build(): - projectdir = sysconfig.get_config_var('projectbase') - pybuilddir = os.path.join(projectdir, 'pybuilddir.txt') - with open(pybuilddir, encoding='utf-8') as f: - dirname = os.path.join(projectdir, f.read()) + if sys.platform == 'win32': + dirname = sysconfig.get_config_var('BINDIR') + else: + projectdir = sysconfig.get_config_var('projectbase') + pybuilddir = os.path.join(projectdir, 'pybuilddir.txt') + with open(pybuilddir, encoding='utf-8') as f: + dirname = os.path.join(projectdir, f.read()) else: dirname = sysconfig.get_path('stdlib') return os.path.join(dirname, 'build-details.json') @@ -176,9 +164,8 @@ def test_c_api(self): @unittest.skipIf( generate_build_details is None, - "Failed to import generate-build-details" + "Failed to import generate_build_details", ) -@unittest.skipIf(os.name != 'posix', 'Feature only implemented on POSIX right now') @unittest.skipIf(is_wasm32, 'Feature not available on WebAssembly builds') class BuildDetailsRelativePathsTests(unittest.TestCase): @property @@ -189,7 +176,7 @@ def build_details_absolute_paths(self): @property def build_details_relative_paths(self): data = self.build_details_absolute_paths - generate_build_details.make_paths_relative(data, config_path=None) + generate_build_details.make_paths_relative(data, base_path=None) return data def test_round_trip(self): diff --git a/Makefile.pre.in b/Makefile.pre.in index 34bd4540efb0b8..62f6d16ac932a7 100644 --- a/Makefile.pre.in +++ b/Makefile.pre.in @@ -1004,7 +1004,7 @@ pybuilddir.txt: $(PYTHON_FOR_BUILD_DEPS) fi build-details.json: pybuilddir.txt - $(RUNSHARED) $(PYTHON_FOR_BUILD) $(srcdir)/Tools/build/generate-build-details.py `cat pybuilddir.txt`/build-details.json + $(RUNSHARED) $(PYTHON_FOR_BUILD) $(srcdir)/Tools/build/generate_build_details.py `cat pybuilddir.txt`/build-details.json # Build static library $(LIBRARY): $(LIBRARY_OBJS) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-09-04-00-32-28.gh-issue-138489.Adc9Lx.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-09-04-00-32-28.gh-issue-138489.Adc9Lx.rst new file mode 100644 index 00000000000000..38de0930767dc5 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-09-04-00-32-28.gh-issue-138489.Adc9Lx.rst @@ -0,0 +1,3 @@ +The :pep:`739` :file:`build-details.json` file is now generated and +installed on Windows. +Patch by Adam Turner. diff --git a/PC/layout/main.py b/PC/layout/main.py index 8543e7c56e1c41..4ccc1a724ed68b 100644 --- a/PC/layout/main.py +++ b/PC/layout/main.py @@ -22,6 +22,7 @@ __path__ = [str(Path(__file__).resolve().parent)] from .support.appxmanifest import * +from .support.build_details import * from .support.catalog import * from .support.constants import * from .support.filesets import * @@ -319,6 +320,9 @@ def _c(d): if ns.include_install_json or ns.include_install_embed_json or ns.include_install_test_json: yield "__install__.json", ns.temp / "__install__.json" + if ns.include_build_details_json: + yield "build-details.json", ns.temp / "build-details.json" + def _compile_one_py(src, dest, name, optimize, checked=True): import py_compile @@ -426,6 +430,12 @@ def generate_source_files(ns): with open(ns.temp / "__install__.json", "w", encoding="utf-8") as f: json.dump(calculate_install_json(ns, for_test=True), f, indent=2) + if ns.include_build_details_json: + log_info("Generating build-details.json in {}", ns.temp) + ns.temp.mkdir(parents=True, exist_ok=True) + base_path = Path(sys.base_prefix, "build-details.json") + write_relative_build_details(ns.temp / "build-details.json", base_path) + def _create_zip_file(ns): if not ns.zip: diff --git a/PC/layout/support/build_details.py b/PC/layout/support/build_details.py new file mode 100644 index 00000000000000..e70e528872290e --- /dev/null +++ b/PC/layout/support/build_details.py @@ -0,0 +1,37 @@ +""" +Generate the PEP 739 'build-details.json' document. +""" + +import sys +from pathlib import Path + +PEP739_SCHEMA_VERSION = '1.0' + +ROOT_DIR = Path( + __file__, # PC/layout/support/build_details.py + '..', # PC/layout/support + '..', # PC/layout + '..', # PC + '..', # +).resolve() +TOOLS_BUILD_DIR = ROOT_DIR / 'Tools' / 'build' + +sys_path = sys.path[:] +try: + sys.path.insert(0, str(TOOLS_BUILD_DIR)) + import generate_build_details +except ImportError: + generate_build_details = None +finally: + sys.path = sys_path + del sys_path + + +def write_relative_build_details(out_path, base_path): + if generate_build_details is None: + return + generate_build_details.write_build_details( + schema_version=PEP739_SCHEMA_VERSION, + base_path=base_path, + location=out_path, + ) diff --git a/PC/layout/support/options.py b/PC/layout/support/options.py index e8c393385425e7..cba1c9fcdcc786 100644 --- a/PC/layout/support/options.py +++ b/PC/layout/support/options.py @@ -39,6 +39,7 @@ def public(f): "install-json": {"help": "a PyManager __install__.json file"}, "install-embed-json": {"help": "a PyManager __install__.json file for embeddable distro"}, "install-test-json": {"help": "a PyManager __install__.json for the test distro"}, + "build-details-json": {"help": "a PEP 739 build-details.json file"}, } @@ -56,6 +57,7 @@ def public(f): "appxmanifest", "alias", "alias3x", + "build-details-json", # XXX: Disabled for now "precompile", ], }, @@ -69,9 +71,10 @@ def public(f): "props", "nuspec", "alias", + "build-details-json", ], }, - "iot": {"help": "Windows IoT Core", "options": ["alias", "stable", "pip"]}, + "iot": {"help": "Windows IoT Core", "options": ["alias", "stable", "pip", "build-details-json"]}, "default": { "help": "development kit package", "options": [ @@ -85,6 +88,7 @@ def public(f): "symbols", "html-doc", "alias", + "build-details-json", ], }, "embed": { @@ -96,6 +100,7 @@ def public(f): "flat-dlls", "underpth", "precompile", + "build-details-json", ], }, "pymanager": { @@ -109,6 +114,7 @@ def public(f): "dev", "html-doc", "install-json", + "build-details-json", ], }, "pymanager-test": { @@ -124,6 +130,7 @@ def public(f): "symbols", "tests", "install-test-json", + "build-details-json", ], }, } diff --git a/PCbuild/python.vcxproj b/PCbuild/python.vcxproj index 70dabaa3c8bc02..c639af2b83141f 100644 --- a/PCbuild/python.vcxproj +++ b/PCbuild/python.vcxproj @@ -126,6 +126,12 @@ + + + + ucrtbase diff --git a/PCbuild/pythoncore.vcxproj b/PCbuild/pythoncore.vcxproj index 517103acea8d8e..15477701bb66bc 100644 --- a/PCbuild/pythoncore.vcxproj +++ b/PCbuild/pythoncore.vcxproj @@ -735,4 +735,7 @@ + + + diff --git a/Tools/build/generate-build-details.py b/Tools/build/generate_build_details.py similarity index 76% rename from Tools/build/generate-build-details.py rename to Tools/build/generate_build_details.py index ed9ab2844d250a..372b81508db1d0 100644 --- a/Tools/build/generate-build-details.py +++ b/Tools/build/generate_build_details.py @@ -12,10 +12,30 @@ import os import sys import sysconfig +from pathlib import Path TYPE_CHECKING = False if TYPE_CHECKING: - from typing import Any + from typing import Any, Literal, TypeAlias + + StrPath: TypeAlias = str | os.PathLike[str] + ValidSchemaVersion: TypeAlias = Literal['1.0'] + + +def write_build_details( + *, + schema_version: ValidSchemaVersion, + base_path: StrPath | None, + location: StrPath, +) -> None: + data = generate_data(schema_version) + if base_path is not None: + make_paths_relative(data, base_path) + + json_output = json.dumps(data, indent=2) + with open(location, 'w', encoding='utf-8') as f: + f.write(json_output) + f.write('\n') def version_info_to_dict(obj: sys._version_info) -> dict[str, Any]: @@ -29,7 +49,9 @@ def get_dict_key(container: dict[str, Any], key: str) -> dict[str, Any]: return container -def generate_data(schema_version: str) -> collections.defaultdict[str, Any]: +def generate_data( + schema_version: ValidSchemaVersion +) -> collections.defaultdict[str, Any]: """Generate the build-details.json data (PEP 739). :param schema_version: The schema version of the data we want to generate. @@ -46,10 +68,16 @@ def generate_data(schema_version: str) -> collections.defaultdict[str, Any]: data['base_prefix'] = sysconfig.get_config_var('installed_base') #data['base_interpreter'] = sys._base_executable - data['base_interpreter'] = os.path.join( - sysconfig.get_path('scripts'), - 'python' + sysconfig.get_config_var('VERSION'), - ) + if os.name == 'nt': + data['base_interpreter'] = os.path.join( + data['base_prefix'], + os.path.basename(sys._base_executable), # type: ignore[attr-defined] + ) + else: + data['base_interpreter'] = os.path.join( + sysconfig.get_path('scripts'), + 'python' + sysconfig.get_config_var('VERSION'), + ) data['platform'] = sysconfig.get_platform() data['language']['version'] = sysconfig.get_python_version() @@ -61,7 +89,10 @@ def generate_data(schema_version: str) -> collections.defaultdict[str, Any]: if '_multiarch' in data['implementation']: data['implementation']['_multiarch'] = sysconfig.get_config_var('MULTIARCH') - data['abi']['flags'] = list(sys.abiflags) + if os.name != 'nt': + data['abi']['flags'] = list(sys.abiflags) + else: + data['abi']['flags'] = [] data['suffixes']['source'] = importlib.machinery.SOURCE_SUFFIXES data['suffixes']['bytecode'] = importlib.machinery.BYTECODE_SUFFIXES @@ -69,13 +100,19 @@ def generate_data(schema_version: str) -> collections.defaultdict[str, Any]: #data['suffixes']['debug_bytecode'] = importlib.machinery.DEBUG_BYTECODE_SUFFIXES data['suffixes']['extensions'] = importlib.machinery.EXTENSION_SUFFIXES - LIBDIR = sysconfig.get_config_var('LIBDIR') + if os.name == 'nt': + LIBDIR = data['base_prefix'] + else: + LIBDIR = sysconfig.get_config_var('LIBDIR') LDLIBRARY = sysconfig.get_config_var('LDLIBRARY') LIBRARY = sysconfig.get_config_var('LIBRARY') PY3LIBRARY = sysconfig.get_config_var('PY3LIBRARY') LIBPYTHON = sysconfig.get_config_var('LIBPYTHON') LIBPC = sysconfig.get_config_var('LIBPC') - INCLUDEPY = sysconfig.get_config_var('INCLUDEPY') + if os.name == 'nt': + INCLUDEPY = os.path.join(data['base_prefix'], 'include') + else: + INCLUDEPY = sysconfig.get_config_var('INCLUDEPY') if os.name == 'posix': # On POSIX, LIBRARY is always the static library, while LDLIBRARY is the @@ -130,11 +167,7 @@ def generate_data(schema_version: str) -> collections.defaultdict[str, Any]: return data -def make_paths_relative(data: dict[str, Any], config_path: str | None = None) -> None: - # Make base_prefix relative to the config_path directory - if config_path: - data['base_prefix'] = relative_path(data['base_prefix'], - os.path.dirname(config_path)) +def make_paths_relative(data: dict[str, Any], base_path: StrPath | None = None) -> None: base_prefix = data['base_prefix'] # Update path values to make them relative to base_prefix @@ -164,8 +197,12 @@ def make_paths_relative(data: dict[str, Any], config_path: str | None = None) -> new_path = os.path.join('.', new_path) container[child] = new_path + if base_path: + # Make base_prefix relative to the base_path directory + config_dir = Path(base_path).resolve().parent + data['base_prefix'] = relative_path(base_prefix, config_dir) -def relative_path(path: str, base: str) -> str: +def relative_path(path: StrPath, base: StrPath) -> str: if os.name != 'nt': return os.path.relpath(path, base) @@ -175,7 +212,7 @@ def relative_path(path: str, base: str) -> str: if path_drv.lower() == base_drv.lower(): return os.path.relpath(path, base) - return path + return os.fspath(path) def main() -> None: @@ -198,15 +235,18 @@ def main() -> None: ) args = parser.parse_args() - - data = generate_data(args.schema_version) - if args.relative_paths: - make_paths_relative(data, args.config_file_path) - - json_output = json.dumps(data, indent=2) - with open(args.location, 'w', encoding='utf-8') as f: - f.write(json_output) - f.write('\n') + if os.name == 'nt': + # Windows builds are relocatable; always make paths relative. + base_path = args.config_file_path or args.location + elif args.relative_paths: + base_path = args.config_file_path + else: + base_path = None + write_build_details( + schema_version=args.schema_version, + base_path=base_path, + location=args.location, + ) if __name__ == '__main__': diff --git a/Tools/build/mypy.ini b/Tools/build/mypy.ini index 331bada6f47d2e..5e5ed9bf9f2eec 100644 --- a/Tools/build/mypy.ini +++ b/Tools/build/mypy.ini @@ -7,7 +7,7 @@ files = Tools/build/check_warnings.py, Tools/build/compute-changes.py, Tools/build/deepfreeze.py, - Tools/build/generate-build-details.py, + Tools/build/generate_build_details.py, Tools/build/generate_sbom.py, Tools/build/generate_stdlib_module_names.py, Tools/build/verify_ensurepip_wheels.py,