diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index bde655f3ba..258cdd2c5f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -40,7 +40,7 @@ jobs: - name: Build and install run: | - python setup.py --skip-verstamp install --user + python setup.py install --user - name: Run tests # Run the tests directly from the source dir so support files (eg, .wav files etc) @@ -91,7 +91,7 @@ jobs: python .github\workflows\download-arm64-libs.py .\arm64libs - name: Build wheels - run: python setup.py --skip-verstamp build_ext -L .\arm64libs --plat-name win-arm64 build --plat-name win-arm64 bdist_wheel --plat-name win-arm64 + run: python setup.py build_ext -L .\arm64libs --plat-name win-arm64 build --plat-name win-arm64 bdist_wheel --plat-name win-arm64 - uses: actions/upload-artifact@v3 if: ${{ always() }} diff --git a/README.md b/README.md index d8e258db91..495cf8c5c5 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ to form a checklist so @mhammond doesn't forget what to do :) * Update setup.py with the new build number. -* Execute `make.bat`, wait forever, test the artifacts. +* Execute `make_all.bat`, wait forever, test the artifacts. * Upload .whl artifacts to pypi - we do this before pushing the tag because they might be rejected for an invalid `README.md`. Done via `py -3.? -m twine upload dist/*XXX*.whl`. diff --git a/mypy.ini b/mypy.ini index a93198a2f6..f2d5ea44a2 100644 --- a/mypy.ini +++ b/mypy.ini @@ -48,11 +48,10 @@ exclude = (?x)( [mypy-adsi.*,dde,exchange,exchdapi,mapi,perfmon,servicemanager,win32api,win32console,win32clipboard,win32comext.adsi.adsi,win32event,win32evtlog,win32file,win32gui,win32help,win32pdh,win32process,win32ras,win32security,win32service,win32trace,win32ui,win32uiole,win32wnet,_win32sysloader,_winxptheme] ignore_missing_imports = True -; verstamp is installed from win32verstamp.py called in setup.py ; Most of win32com re-exports win32comext ; Test is a local untyped module in win32comext.axdebug ; pywin32_system32 is an empty module created in setup.py to store dlls -[mypy-verstamp,win32com.*,Test,pywin32_system32] +[mypy-win32com.*,Test,pywin32_system32] ignore_missing_imports = True ; Distutils being removed from stdlib currently causes some issues on Python 3.12 diff --git a/setup.py b/setup.py index 6ce1ed77de..1132c2cc55 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ __doc__ = """This is a distutils setup-script for the pywin32 extensions. The canonical source of truth for supported versions and build environments -is [the github CI](https://github.com/mhammond/pywin32/tree/main/.github/workflows). +is [the GitHub CI](https://github.com/mhammond/pywin32/tree/main/.github/workflows). To build and install locally for testing etc, you need a build environment which is capable of building the version of Python you are targeting, then: @@ -62,12 +62,6 @@ ) print("Building pywin32", pywin32_version) -try: - sys.argv.remove("--skip-verstamp") - skip_verstamp = True -except ValueError: - skip_verstamp = False - try: this_file = __file__ except NameError: @@ -985,35 +979,18 @@ def link( # target. Do this externally to avoid suddenly dragging in the # modules needed by this process, and which we will soon try and # update. - # Further, we don't really want to use sys.executable, because that - # means the build environment must have a current pywin32 installed - # in every version, which is a bit of a burden only for this. - # So we assume the "default" Python version (ie, the version run by - # py.exe) has pywin32 installed. - # (This creates a chicken-and-egg problem though! We used to work around - # this by ignoring failure to verstamp, but that's easy to miss. So now - # allow --skip-verstamp on the cmdline - but if it's not there, the - # verstamp must work.) - if not skip_verstamp: - args = ["py.exe", "-m", "win32verstamp"] - args.append(f"--version={pywin32_version}") - args.append("--comments=https://github.com/mhammond/pywin32") - args.append(f"--original-filename={os.path.basename(output_filename)}") - args.append("--product=PyWin32") - if "-v" not in sys.argv: - args.append("--quiet") - args.append(output_filename) - try: - self.spawn(args) - except Exception: - print("** Failed to versionstamp the binaries.") - # py.exe is not yet available for windows-arm64 so version stamp will fail - # ignore it for now - if platform.machine() != "ARM64": - print( - "** If you want to skip this step, pass '--skip-verstamp' on the setup.py command-line" - ) - raise + args = [ + sys.executable, + # NOTE: On Python 3.7, all args must be str + str(Path(__file__).parent / "win32" / "Lib" / "win32verstamp.py"), + f"--version={pywin32_version}", + "--comments=https://github.com/mhammond/pywin32", + f"--original-filename={os.path.basename(output_filename)}", + "--product=PyWin32", + "--quiet" if "-v" not in sys.argv else "", + output_filename, + ] + self.spawn(args) # Work around bpo-36302/bpo-42009 - it sorts sources but this breaks # support for building .mc files etc :( diff --git a/win32/Lib/_win32verstamp_pywin32ctypes.py b/win32/Lib/_win32verstamp_pywin32ctypes.py new file mode 100644 index 0000000000..5fb9398a8e --- /dev/null +++ b/win32/Lib/_win32verstamp_pywin32ctypes.py @@ -0,0 +1,207 @@ +""" +A pure-python re-implementation of methods used by win32verstamp. +This is to avoid a bootstraping problem where win32verstamp is used during build, +but requires an installation of pywin32 to be present. +We used to work around this by ignoring failure to verstamp, but that's easy to miss. + +Implementations adapted, simplified and typed from: +- https://github.com/enthought/pywin32-ctypes/blob/main/win32ctypes/core/ctypes/_util.py +- https://github.com/enthought/pywin32-ctypes/blob/main/win32ctypes/core/cffi/_resource.py +- https://github.com/enthought/pywin32-ctypes/blob/main/win32ctypes/pywin32/win32api.py + +--- + +(C) Copyright 2014 Enthought, Inc., Austin, TX +All right reserved. + +This file is open source software distributed according to the terms in +https://github.com/enthought/pywin32-ctypes/blob/main/LICENSE.txt +""" + +from __future__ import annotations + +from collections.abc import Callable, Iterable +from ctypes import FormatError, WinDLL, _SimpleCData, get_last_error +from ctypes.wintypes import ( + BOOL, + DWORD, + HANDLE, + LPCWSTR, + LPVOID, + WORD, +) +from typing import TYPE_CHECKING, Any, SupportsBytes, SupportsIndex + +if TYPE_CHECKING: + from ctypes import _NamedFuncPointer + + from _typeshed import ReadableBuffer + from typing_extensions import Literal + +kernel32 = WinDLL("kernel32", use_last_error=True) + +### +# https://github.com/enthought/pywin32-ctypes/blob/main/win32ctypes/core/ctypes/_util.py +### + + +def function_factory( + function: _NamedFuncPointer, + argument_types: list[type[_SimpleCData[Any]]], + return_type: type[_SimpleCData[Any]], + error_checking: Callable[..., Any], # Incomplete +) -> _NamedFuncPointer: + function.argtypes = argument_types + function.restype = return_type + function.errcheck = error_checking + return function + + +def make_error(function: _NamedFuncPointer) -> OSError: + code = get_last_error() + description = FormatError(code).strip() + function_name = function.__name__ + exception = OSError() + exception.winerror = code + exception.function = function_name + exception.strerror = description + return exception + + +def check_null(result: int | None, function: _NamedFuncPointer, *_) -> int: + if result is None: + raise make_error(function) + return result + + +def check_false(result: int | None, function: _NamedFuncPointer, *_) -> Literal[True]: + if not bool(result): + raise make_error(function) + else: + return True + + +### +# https://github.com/enthought/pywin32-ctypes/blob/main/win32ctypes/core/cffi/_resource.py +### + + +def _UpdateResource( + hUpdate: int, + lpType: str | int, + lpName: str | int, + wLanguage: int, + lpData: bytes, + cbData: int, +): + lp_type = LPCWSTR(lpType) + lp_name = LPCWSTR(lpName) + _BaseUpdateResource(hUpdate, lp_type, lp_name, wLanguage, lpData, cbData) + + +_BeginUpdateResource = function_factory( + kernel32.BeginUpdateResourceW, + [LPCWSTR, BOOL], + HANDLE, + check_null, +) + + +_EndUpdateResource = function_factory( + kernel32.EndUpdateResourceW, + [HANDLE, BOOL], + BOOL, + check_false, +) + +_BaseUpdateResource = function_factory( + kernel32.UpdateResourceW, + [HANDLE, LPCWSTR, LPCWSTR, WORD, LPVOID, DWORD], + BOOL, + check_false, +) + + +### +# https://github.com/enthought/pywin32-ctypes/blob/main/win32ctypes/pywin32/win32api.py +### + +LANG_NEUTRAL = 0x00 + + +def BeginUpdateResource(filename: str, delete: bool): + """Get a handle that can be used by the :func:`UpdateResource`. + + Parameters + ---------- + fileName : unicode + The filename of the module to load. + delete : bool + When true all existing resources are deleted + + Returns + ------- + result : hModule + Handle of the resource. + + """ + return _BeginUpdateResource(filename, delete) + + +def EndUpdateResource(handle: int, discard: bool) -> None: + """End the update resource of the handle. + + Parameters + ---------- + handle : hModule + The handle of the resource as it is returned + by :func:`BeginUpdateResource` + + discard : bool + When True all writes are discarded. + + """ + _EndUpdateResource(handle, discard) + + +def UpdateResource( + handle: int, + type: str | int, + name: str | int, + data: Iterable[SupportsIndex] | SupportsIndex | SupportsBytes | ReadableBuffer, + language=LANG_NEUTRAL, +) -> None: + """Update a resource. + + Parameters + ---------- + handle : hModule + The handle of the resource file as returned by + :func:`BeginUpdateResource`. + + type : str : int + The type of resource to update. + + name : str : int + The name or Id of the resource to update. + + data : bytes + A bytes like object is expected. + + .. note:: + PyWin32 version 219, on Python 2.7, can handle unicode inputs. + However, the data are stored as bytes and it is not really + possible to convert the information back into the original + unicode string. To be consistent with the Python 3 behaviour + of PyWin32, we raise an error if the input cannot be + converted to `bytes`. + + language : int + Language to use, default is LANG_NEUTRAL. + + """ + try: + lp_data = bytes(data) + except UnicodeEncodeError: + raise TypeError("a bytes-like object is required, not a 'unicode'") + _UpdateResource(handle, type, name, language, lp_data, len(lp_data)) diff --git a/win32/Lib/win32verstamp.py b/win32/Lib/win32verstamp.py index e9f8c5e45d..7b211303db 100644 --- a/win32/Lib/win32verstamp.py +++ b/win32/Lib/win32verstamp.py @@ -1,12 +1,15 @@ -""" Stamp a Win32 binary with version information. -""" +"""Stamp a Win32 binary with version information.""" import glob import optparse import os import struct -from win32api import BeginUpdateResource, EndUpdateResource, UpdateResource +from _win32verstamp_pywin32ctypes import ( + BeginUpdateResource, + EndUpdateResource, + UpdateResource, +) VS_FFI_SIGNATURE = -17890115 # 0xFEEF04BD VS_FFI_STRUCVERSION = 0x00010000 diff --git a/win32/scripts/VersionStamp/bulkstamp.py b/win32/scripts/VersionStamp/bulkstamp.py index 3a039ebb81..49bdc3e930 100644 --- a/win32/scripts/VersionStamp/bulkstamp.py +++ b/win32/scripts/VersionStamp/bulkstamp.py @@ -33,9 +33,15 @@ import fnmatch import os import sys +from collections.abc import Mapping +from optparse import Values -import verstamp -import win32api +try: + import win32verstamp +except ModuleNotFoundError: + # If run with pywin32 not already installed + sys.path.append(os.path.abspath(__file__ + "/../../../Lib")) + import win32verstamp numStamped = 0 @@ -47,9 +53,8 @@ ] -def walk(arg, dirname, names): +def walk(vars: Mapping[str, str], debug, descriptions, dirname, names): global numStamped - vars, debug, descriptions = arg for name in names: for pat in g_patterns: if fnmatch.fnmatch(name, pat): @@ -60,18 +65,21 @@ def walk(arg, dirname, names): name = base[:-2] + ext is_dll = ext.lower() != ".exe" if os.path.normcase(name) in descriptions: - desc = descriptions[os.path.normcase(name)] + description = descriptions[os.path.normcase(name)] try: - verstamp.stamp(vars, pathname, desc, is_dll=is_dll) + options = Values( + {**vars, "description": description, "dll": is_dll} + ) + win32verstamp.stamp(pathname, options) numStamped += 1 - except win32api.error as exc: + except Exception as exc: print( "Could not stamp", pathname, - "Error", - exc.winerror, + "with", + options, "-", - exc.strerror, + repr(exc), ) else: print("WARNING: description not provided for:", name) @@ -82,7 +90,7 @@ def walk(arg, dirname, names): def load_descriptions(fname, vars): - retvars = {} + retvars: dict[str, str] = {} descriptions = {} lines = open(fname, "r").readlines() @@ -118,7 +126,8 @@ def load_descriptions(fname, vars): return retvars, descriptions -def scan(build, root, desc, **custom_vars): +def scan(build, root: str, desc, **custom_vars): + print(build, root, desc) global numStamped numStamped = 0 try: @@ -135,8 +144,8 @@ def scan(build, root, desc, **custom_vars): vars["build"] = build vars.update(custom_vars) - arg = vars, debug, descriptions - os.path.walk(root, walk, arg) + for directory, dirnames, filenames in os.walk(root): + walk(vars, debug, descriptions, directory, filenames) print("Stamped %d files." % (numStamped)) @@ -146,4 +155,4 @@ def scan(build, root, desc, **custom_vars): print("ERROR: incorrect invocation. See script's header comments.") sys.exit(1) - scan(*tuple(sys.argv[1:])) + scan(*sys.argv[1:])