From 335748cd57461507c383ee795a7cdba5e18ffe7c Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Fri, 16 May 2025 16:57:14 +0100 Subject: [PATCH] Adopt pymsbuild-msix for building --- .github/workflows/build.yml | 2 +- _make_helper.py | 20 ++++++----- _msbuild.py | 8 +++-- ci/release.yml | 2 +- make-msi.py | 6 ++-- make-msix.py | 69 +++++++++---------------------------- make.py | 27 +++++++++++---- 7 files changed, 59 insertions(+), 75 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7412d78..8c7ad84 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -56,7 +56,7 @@ jobs: sys.exit(0 if sys.version_info[:5] == (3, 14, 0, 'beta', 1) else 1)" - name: Install build dependencies - run: python -m pip install "pymsbuild>=1.2.0b1" + run: python -m pip install "pymsbuild>=1.2.2b1" "pymsbuild-msix>=0.1.0a1" - name: 'Install test runner' run: python -m pip install pytest pytest-cov diff --git a/_make_helper.py b/_make_helper.py index ee27ba9..356633d 100644 --- a/_make_helper.py +++ b/_make_helper.py @@ -86,11 +86,10 @@ def get_dirs(): if not _layout: _layout = _temp / "layout" os.environ["PYMSBUILD_LAYOUT_DIR"] = str(_layout) - out = _layout / "python-manager" return dict( root=root, - out=out, + out=_layout, src=src, dist=dist, build=build, @@ -98,10 +97,10 @@ def get_dirs(): ) -def get_msix_version(dirs): +def get_msix_version(manifest): from io import StringIO from xml.etree import ElementTree as ET - appx = (dirs["out"] / "appxmanifest.xml").read_text("utf-8") + appx = Path(manifest).read_text("utf-8") NS = dict(e for _, e in ET.iterparse(StringIO(appx), events=("start-ns",))) for k, v in NS.items(): ET.register_namespace(k, v) @@ -110,10 +109,15 @@ def get_msix_version(dirs): return identity.attrib['Version'] -def get_output_name(dirs): - with open(dirs["out"] / "version.txt", "r", encoding="utf-8") as f: - version = f.read().strip() - return f"python-manager-{version}" +def get_output_name(layout): + with open(layout / "__state.txt", "r") as f: + for line in f: + if line == "# BEGIN FILES": + break + key, sep, value = line.partition("=") + if sep and key == "msix_name": + return value.rpartition(".")[0] + return "python-manager" copyfile = shutil.copyfile diff --git a/_msbuild.py b/_msbuild.py index 387a6cd..370be29 100644 --- a/_msbuild.py +++ b/_msbuild.py @@ -1,6 +1,7 @@ import os from pymsbuild import * from pymsbuild.dllpack import * +from pymsbuild_msix import AppxManifest, AppInstaller, ResourcesXml DLL_NAME = "python314" @@ -15,7 +16,7 @@ def can_embed(tag): METADATA = { "Metadata-Version": "2.2", - "Name": "manage", + "Name": "python-manager", "Version": "0.1a0", "Author": "Python Software Foundation", "Author-email": "steve.dower@python.org", @@ -179,8 +180,8 @@ def pyshellext(ext='.exe', **props): PACKAGE = Package('python-manager', PyprojectTomlFile('pyproject.toml'), # MSIX manifest - File('src/pymanager/appxmanifest.xml'), - File('src/pymanager/pymanager.appinstaller'), + AppxManifest('src/pymanager/appxmanifest.xml'), + AppInstaller('src/pymanager/pymanager.appinstaller'), Package( 'MSIX.AppInstaller.Data', File('src/pymanager/MSIXAppInstallerData.xml'), @@ -208,6 +209,7 @@ def pyshellext(ext='.exe', **props): ), # Directory for MSIX resources + ResourcesXml('src/pymanager/resources.xml'), Package( '_resources', File('src/pymanager/_resources/*.png'), diff --git a/ci/release.yml b/ci/release.yml index 4c3bdf8..b6f37d9 100644 --- a/ci/release.yml +++ b/ci/release.yml @@ -93,7 +93,7 @@ stages: workingDirectory: $(Build.BinariesDirectory) - powershell: | - python -m pip install "pymsbuild>=1.2.0b1" + python -m pip install "pymsbuild>=1.2.2b1" "pymsbuild-msix>=0.1.0a1" displayName: 'Install build dependencies' - ${{ if eq(parameters.PreTest, 'true') }}: diff --git a/make-msi.py b/make-msi.py index 1072a26..3003586 100644 --- a/make-msi.py +++ b/make-msi.py @@ -12,13 +12,13 @@ DIRS = get_dirs() BUILD = DIRS["build"] TEMP = DIRS["temp"] -LAYOUT = DIRS["out"] +LAYOUT = DIRS["out"] / "python-manager" SRC = DIRS["src"] DIST = DIRS["dist"] # Calculate output names (must be after building) -NAME = get_output_name(DIRS) -VERSION = get_msix_version(DIRS) +NAME = get_output_name(DIRS["out"]) +VERSION = get_msix_version(LAYOUT / "appxmanifest.xml") # Package into MSI pydllname = [p.stem for p in (LAYOUT / "runtime").glob("python*.dll")][0] diff --git a/make-msix.py b/make-msix.py index cc07fdc..12a979a 100644 --- a/make-msix.py +++ b/make-msix.py @@ -9,23 +9,11 @@ copyfile, copytree, get_dirs, - get_msix_version, get_output_name, - get_sdk_bins, rmtree, unlink, ) -SDK_BINS = get_sdk_bins() - -MAKEAPPX = SDK_BINS / "makeappx.exe" -MAKEPRI = SDK_BINS / "makepri.exe" - -for tool in [MAKEAPPX, MAKEPRI]: - if not tool.is_file(): - print("Unable to locate Windows Kit tool", tool.name, file=sys.stderr) - sys.exit(3) - DIRS = get_dirs() BUILD = DIRS["build"] TEMP = DIRS["temp"] @@ -35,51 +23,25 @@ DIST = DIRS["dist"] # Calculate output names (must be after building) -NAME = get_output_name(DIRS) -VERSION = get_msix_version(DIRS) -DIST_MSIX = DIST / f"{NAME}.msix" -DIST_STORE_MSIX = DIST / f"{NAME}-store.msix" -DIST_APPXSYM = DIST / f"{NAME}-store.appxsym" -DIST_MSIXUPLOAD = DIST / f"{NAME}-store.msixupload" +DIST_MSIX = DIST / get_output_name(LAYOUT) +DIST_STORE_MSIX = DIST_MSIX.with_name(f"{DIST_MSIX.stem}-store.msix") +DIST_APPXSYM = DIST_STORE_MSIX.with_suffix(".appxsym") +DIST_MSIXUPLOAD = DIST_STORE_MSIX.with_suffix(".msixupload") unlink(DIST_MSIX, DIST_STORE_MSIX, DIST_APPXSYM, DIST_MSIXUPLOAD) -# Generate resources info in LAYOUT -if not (LAYOUT / "_resources.pri").is_file(): - run([MAKEPRI, "new", "/o", - "/pr", LAYOUT, - "/cf", SRC / "pymanager/resources.xml", - "/of", LAYOUT / "_resources.pri", - "/mf", "appx"]) - -# Clean up non-shipping files from LAYOUT -preserved = [ - *LAYOUT.glob("pyshellext*.dll"), -] - -for f in preserved: - print("Preserving", f, "as", TEMP / f.name) - copyfile(f, TEMP / f.name) - -unlink( - *LAYOUT.rglob("*.pdb"), - *LAYOUT.rglob("*.pyc"), - *LAYOUT.rglob("__pycache__"), - *preserved, -) - # Package into DIST -run([MAKEAPPX, "pack", "/o", "/d", LAYOUT, "/p", DIST_MSIX]) +run([sys.executable, "-m", "pymsbuild", "pack", "-v"]) print("Copying appinstaller file to", DIST) -copyfile(LAYOUT / "pymanager.appinstaller", DIST / "pymanager.appinstaller") +copyfile(LAYOUT / "python-manager/pymanager.appinstaller", DIST / "pymanager.appinstaller") if os.getenv("PYMANAGER_APPX_STORE_PUBLISHER"): # Clone and update layout for Store build rmtree(LAYOUT2) copytree(LAYOUT, LAYOUT2) - unlink(*LAYOUT2.glob("*.appinstaller")) + unlink(*LAYOUT2.glob("python-manager/*.appinstaller")) def patch_appx(source): from xml.etree import ElementTree as ET @@ -116,9 +78,16 @@ def patch_appx(source): with open(source, "wb") as f: xml.write(f, "utf-8") - patch_appx(LAYOUT2 / "appxmanifest.xml") + patch_appx(LAYOUT2 / "python-manager/appxmanifest.xml") - run([MAKEAPPX, "pack", "/o", "/d", LAYOUT2, "/p", DIST_STORE_MSIX]) + run( + [sys.executable, "-m", "pymsbuild", "pack", "-v"], + env={ + **os.environ, + "PYMSBUILD_LAYOUT_DIR": str(LAYOUT2), + "PYMSBUILD_MSIX_NAME": DIST_STORE_MSIX.name, + } + ) # Pack symbols print("Packing symbols to", DIST_APPXSYM) @@ -131,9 +100,3 @@ def patch_appx(source): with zipfile.ZipFile(DIST_MSIXUPLOAD, "w") as zf: zf.write(DIST_STORE_MSIX, arcname=DIST_STORE_MSIX.name) zf.write(DIST_APPXSYM, arcname=DIST_APPXSYM.name) - - -for f in preserved: - print("Restoring", f, "from", TEMP / f.name) - copyfile(TEMP / f.name, f) - unlink(TEMP / f.name) diff --git a/make.py b/make.py index 1da9cc2..cf06db2 100644 --- a/make.py +++ b/make.py @@ -1,6 +1,7 @@ import os import subprocess import sys +from pathlib import PurePath from subprocess import check_call as run from _make_helper import get_dirs, rmtree, unlink @@ -38,13 +39,27 @@ pass # Run main build - this fills in BUILD and LAYOUT -run([sys.executable, "-m", "pymsbuild", "wheel"], +run([sys.executable, "-m", "pymsbuild", "msix"], cwd=DIRS["root"], env={**os.environ, "BUILD_SOURCEBRANCH": ref}) # Bundle current latest release -run([LAYOUT / "py-manager.exe", "install", "-v", "-f", "--download", TEMP / "bundle", "default"]) -(LAYOUT / "bundled").mkdir(parents=True, exist_ok=True) -(TEMP / "bundle" / "index.json").rename(LAYOUT / "bundled" / "fallback-index.json") -for f in (TEMP / "bundle").iterdir(): - f.rename(LAYOUT / "bundled" / f.name) +run([LAYOUT / "py-manager.exe", "install", "-v", "-f", "--download", LAYOUT / "bundled", "default"]) +(LAYOUT / "bundled" / "index.json").rename(LAYOUT / "bundled" / "fallback-index.json") + +# Update package state for when we pack +new_lines = [] +state_txt = LAYOUT.parent / "__state.txt" +for line in state_txt.read_text("utf-8").splitlines(): + if not line or "=" in line or line.startswith("#"): + new_lines.append(line) + continue + # Exclude the in-proc shell extension from the MSIX + if PurePath(line).match("pyshellext*.dll"): + continue + new_lines.append(line) +# Include the bundled files in the MSIX +for f in LAYOUT.rglob(r"bundled\*"): + new_lines.append(str(f.relative_to(state_txt.parent))) + +state_txt.write_text("\n".join(new_lines), encoding="utf-8")