diff --git a/.github/workflows/test-installer.yml b/.github/workflows/test-installer.yml index 7e540c8..e0b82f3 100644 --- a/.github/workflows/test-installer.yml +++ b/.github/workflows/test-installer.yml @@ -9,16 +9,29 @@ on: workflow_dispatch: env: - # VS 2022 Enterprise and Strawberry Perl are preinstalled on windows-2025. - VCVARS: 'C:\Program Files\Microsoft Visual Studio\18\Enterprise\VC\Auxiliary\Build\vcvars64.bat' - # The NIST-validated FIPS module is built once from this ref (VC-WIN64A) and - # shared by every installer flavor via the `fips` job's artifact. + # The NIST-validated FIPS module is built once per architecture from this ref + # and shared by every installer flavor of that architecture via the `fips` + # job's artifact. FIPS_REF: openssl-3.1.2 +# Visual Studio Enterprise and Perl are preinstalled on the hosted Windows +# runners, but the VS version and path differ between images (VS 2026 under +# "Microsoft Visual Studio\18" on windows-2025, VS 2022 17.14 with the +# VC.Tools.ARM64 component under "Microsoft Visual Studio\2022" on +# windows-11-arm, see actions/runner-images' Windows11-Arm64-Readme.md), so +# each job locates it with vswhere (the "Locate Visual Studio" steps) and picks +# the vcvars script for its target architecture. jobs: fips: - name: Build FIPS module - runs-on: windows-2025 + name: Build FIPS module (${{ matrix.arch }}) + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - { arch: x64, runner: windows-2025, target: VC-WIN64A, vcvars: vcvars64.bat } + # Native arm64-hosted toolset first, x86-hosted cross compiler as fallback. + - { arch: arm64, runner: windows-11-arm, target: VC-WIN64-ARM, vcvars: "vcvarsarm64.bat vcvarsx86_arm64.bat" } steps: - name: Checkout installer uses: actions/checkout@v7 @@ -28,7 +41,7 @@ jobs: uses: actions/cache@v5 with: path: openssl-fips - key: openssl-fips-${{ runner.os }}-${{ env.FIPS_REF }}-v1 + key: openssl-fips-${{ runner.os }}-${{ matrix.arch }}-${{ env.FIPS_REF }}-v1 - name: Checkout OpenSSL for FIPS (${{ env.FIPS_REF }}) if: steps.fips_cache.outputs.cache-hit != 'true' @@ -50,9 +63,25 @@ jobs: git apply --verbose "$env:GITHUB_WORKSPACE\windows-installer\0001-3.1-Windows-Use-Z7-compiler-flag-to-enable-parallel-.patch" if ($LASTEXITCODE -ne 0) { throw "jom patch failed" } - - name: install nasm + - name: Locate Visual Studio if: steps.fips_cache.outputs.cache-hit != 'true' shell: pwsh + run: | + $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" + $vs = & $vswhere -latest -products * -property installationPath + if (-not $vs) { throw "Visual Studio not found" } + $script = "${{ matrix.vcvars }}".Split(' ') | + ForEach-Object { Join-Path $vs "VC\Auxiliary\Build\$_" } | + Where-Object { Test-Path $_ } | + Select-Object -First 1 + if (-not $script) { throw "none of '${{ matrix.vcvars }}' found under $vs\VC\Auxiliary\Build" } + Write-Host "using $script" + "VCVARS=$script" >> $env:GITHUB_ENV + + - name: install nasm + # x86-only assembler; the ARM64 target assembles with MSVC's armasm64. + if: steps.fips_cache.outputs.cache-hit != 'true' && matrix.arch == 'x64' + shell: pwsh run: | $installer = "nasm-3.01-installer-x64.exe" Invoke-WebRequest -Uri "https://openssl-library.org/ci-deps/$installer" -OutFile $installer @@ -73,19 +102,19 @@ jobs: if ($actual -ne $expected) { throw "SHA256 mismatch for jom.exe (expected $expected, got $actual)" } "C:\jom" | Out-File -FilePath "$env:GITHUB_PATH" -Append - - name: Build FIPS module (VC-WIN64A) + - name: Build FIPS module (${{ matrix.target }}) if: steps.fips_cache.outputs.cache-hit != 'true' shell: cmd working-directory: openssl-fips run: | call "%VCVARS%" - perl Configure VC-WIN64A enable-fips no-makedepend + perl Configure ${{ matrix.target }} enable-fips no-makedepend jom /j4 /S - name: Upload FIPS providers uses: actions/upload-artifact@v4 with: - name: openssl-fips-providers + name: openssl-fips-providers-${{ matrix.arch }} path: | openssl-fips/providers/fips.dll openssl-fips/providers/fips.lib @@ -93,16 +122,18 @@ jobs: if-no-files-found: error build: - name: Build ${{ matrix.crt.flavor }} installers (${{ matrix.openssl-ref }}) + name: Build ${{ matrix.crt.arch }} ${{ matrix.crt.flavor }} installers (${{ matrix.openssl-ref }}) needs: fips - runs-on: windows-2025 + runs-on: ${{ matrix.crt.runner }} strategy: fail-fast: false matrix: - openssl-ref: [openssl-4.0] + openssl-ref: [openssl-4.1] crt: - - { flavor: vs, target: VC-WIN64A, exe_build: ExeBuild, msi_build: MsiBuild, keep: OpenSSL-x64-VS-* } - - { flavor: hybrid, target: VC-WIN64A-HYBRIDCRT, exe_build: ExeBuild_hybrid, msi_build: MsiBuild_hybrid, keep: OpenSSL-x64-hybridCRT-* } + - { arch: x64, runner: windows-2025, vcvars: vcvars64.bat, flavor: vs, target: VC-WIN64A, exe_build: ExeBuild, msi_build: MsiBuild, keep: OpenSSL-x64-VS-* } + - { arch: x64, runner: windows-2025, vcvars: vcvars64.bat, flavor: hybrid, target: VC-WIN64A-HYBRIDCRT, exe_build: ExeBuild_hybrid, msi_build: MsiBuild_hybrid, keep: OpenSSL-x64-hybridCRT-* } + # Hybrid flavor only: every arm64 Windows release ships the Universal CRT. + - { arch: arm64, runner: windows-11-arm, vcvars: "vcvarsarm64.bat vcvarsx86_arm64.bat", flavor: hybrid, target: VC-WIN64-ARM-HYBRIDCRT, exe_build: ExeBuild_arm64_hybrid, msi_build: MsiBuild_arm64_hybrid, keep: OpenSSL-arm64-hybridCRT-* } steps: - name: Checkout installer uses: actions/checkout@v7 @@ -120,18 +151,20 @@ jobs: shell: pwsh working-directory: openssl run: | - # Pick the highest patch-version tag in this branch's family. + # Pick the highest tag in this branch's family; versionsort.suffix + # ranks pre-releases (-alpha1, -beta1) below the final release. $pattern = '${{ matrix.openssl-ref }}.*' - $tag = git tag --list $pattern --sort=-version:refname | Select-Object -First 1 + $tag = git -c versionsort.suffix=- tag --list $pattern --sort=-version:refname | Select-Object -First 1 if (-not $tag) { throw "no tags matching '$pattern' found in the openssl checkout" } $version = $tag -replace '^openssl-', '' - # Remove "-beta1", "-dev" and such + # The installer version is numeric: remove "-beta1", "-dev" and such $version = $version.Split('-')[0] $parts = $version.Split('.') if ($parts.Count -lt 3) { throw "unexpected tag format: $tag" } Write-Host "detected version: $version (from tag $tag)" + "tag=$tag" >> $env:GITHUB_OUTPUT "version=$version" >> $env:GITHUB_OUTPUT "major=$($parts[0])" >> $env:GITHUB_OUTPUT "minor=$($parts[1])" >> $env:GITHUB_OUTPUT @@ -140,18 +173,34 @@ jobs: - name: Check out detected tag shell: cmd working-directory: openssl - run: git -c advice.detachedHead=false checkout tags/openssl-${{ steps.openssl_version.outputs.version }} + run: git -c advice.detachedHead=false checkout tags/${{ steps.openssl_version.outputs.tag }} - name: Restore OpenSSL build cache id: openssl_cache uses: actions/cache@v5 with: path: openssl - key: openssl-build-${{ runner.os }}-${{ matrix.crt.flavor }}-${{ matrix.openssl-ref }}-${{ steps.openssl_version.outputs.version }}-v1 + key: openssl-build-${{ runner.os }}-${{ matrix.crt.arch }}-${{ matrix.crt.flavor }}-${{ steps.openssl_version.outputs.tag }}-v1 - - name: install nasm + - name: Locate Visual Studio if: steps.openssl_cache.outputs.cache-hit != 'true' shell: pwsh + run: | + $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" + $vs = & $vswhere -latest -products * -property installationPath + if (-not $vs) { throw "Visual Studio not found" } + $script = "${{ matrix.crt.vcvars }}".Split(' ') | + ForEach-Object { Join-Path $vs "VC\Auxiliary\Build\$_" } | + Where-Object { Test-Path $_ } | + Select-Object -First 1 + if (-not $script) { throw "none of '${{ matrix.crt.vcvars }}' found under $vs\VC\Auxiliary\Build" } + Write-Host "using $script" + "VCVARS=$script" >> $env:GITHUB_ENV + + - name: install nasm + # x86-only assembler; the ARM64 target assembles with MSVC's armasm64. + if: steps.openssl_cache.outputs.cache-hit != 'true' && matrix.crt.arch == 'x64' + shell: pwsh run: | $installer = "nasm-3.01-installer-x64.exe" Invoke-WebRequest -Uri "https://openssl-library.org/ci-deps/$installer" -OutFile $installer @@ -172,14 +221,6 @@ jobs: if ($actual -ne $expected) { throw "SHA256 mismatch for jom.exe (expected $expected, got $actual)" } "C:\jom" | Out-File -FilePath "$env:GITHUB_PATH" -Append - - name: Apply jom build patches to older than 4.1 - if: steps.openssl_cache.outputs.cache-hit != 'true' - shell: pwsh - working-directory: openssl - run: | - git apply --verbose "$env:GITHUB_WORKSPACE\windows-installer\0001-Windows-Use-Z7-compiler-flag-to-enable-parallel-buil.patch" - if ($LASTEXITCODE -ne 0) { throw "jom patch failed" } - - name: Build OpenSSL (${{ matrix.crt.target }}) if: steps.openssl_cache.outputs.cache-hit != 'true' shell: cmd @@ -193,10 +234,11 @@ jobs: - name: Download FIPS providers uses: actions/download-artifact@v4 with: - name: openssl-fips-providers + name: openssl-fips-providers-${{ matrix.crt.arch }} path: openssl-fips/providers - name: Build installers + # Advanced Installer is an x86 application; on the arm64 runner it runs under emulation. uses: caphyon/advinst-github-action@7edde34c6ff935e53e3de72a5699efcfceb5f6c6 # v2.0.3 (current main HEAD) with: advinst-version: '23.8' @@ -215,7 +257,7 @@ jobs: - name: Upload installers uses: actions/upload-artifact@v4 with: - name: installers-${{ matrix.crt.flavor }}-${{ matrix.openssl-ref }} + name: installers-${{ matrix.crt.arch }}-${{ matrix.crt.flavor }}-${{ matrix.openssl-ref }} path: | build-target/Installer64/${{ matrix.crt.keep }}.exe build-target/Installer64/${{ matrix.crt.keep }}.msi @@ -228,12 +270,16 @@ jobs: fail-fast: false matrix: os: [windows-2022, windows-2025] - openssl-ref: [openssl-4.0] + openssl-ref: [openssl-4.1] installer: - OpenSSL-x64-VS-*.exe - OpenSSL-x64-VS-*.msi - OpenSSL-x64-hybridCRT-*.exe - OpenSSL-x64-hybridCRT-*.msi + include: + # ARM64 packages only install on ARM64 Windows. + - { os: windows-11-arm, openssl-ref: openssl-4.1, installer: OpenSSL-arm64-hybridCRT-*.exe } + - { os: windows-11-arm, openssl-ref: openssl-4.1, installer: OpenSSL-arm64-hybridCRT-*.msi } runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v7 @@ -251,6 +297,24 @@ jobs: enable-cache: true cache-dependency-glob: "" # disables lockfile requirement + # On the arm64 runner uv would otherwise pick an x86_64 CPython that runs + # under emulation, and the tests would drive Windows Installer from an + # emulated process. Use the image's native arm64 Python instead. + - name: Set up native arm64 Python + if: matrix.os == 'windows-11-arm' + uses: actions/setup-python@v5 + with: + python-version: '3.13' + architecture: arm64 + + - name: Use native arm64 Python for uv + if: matrix.os == 'windows-11-arm' + shell: pwsh + run: | + $python = Join-Path $env:pythonLocation "python.exe" + & $python -c "import platform, sys; print(sys.executable, platform.machine())" + "UV_PYTHON=$python" >> $env:GITHUB_ENV + - name: Install Python deps run: uv sync --frozen || uv sync diff --git a/tests/config.yaml b/tests/config.yaml index c64f287..536e620 100644 --- a/tests/config.yaml +++ b/tests/config.yaml @@ -7,7 +7,10 @@ fips: validated_versions: "3.1.2" paths: + # x64 and arm64 packages install_root: 'C:\Program Files\OpenSSL Library' + # x86 (32-bit) packages + install_root_x86: 'C:\Program Files (x86)\OpenSSL Library' registry: # Two key paths the installer writes; the second uses Wow6432Node directly @@ -16,6 +19,11 @@ registry: paths: - 'SOFTWARE\OpenSSL Corporation\OpenSSL-{registry_version}-OpenSSLProject' - 'SOFTWARE\Wow6432Node\OpenSSL-{registry_version}-OpenSSLProject' + # The same two writes made by a 32-bit package on 64-bit Windows: the + # registry redirector sends both into Wow6432Node (as seen from 64-bit Python). + paths_x86: + - 'SOFTWARE\Wow6432Node\OpenSSL Corporation\OpenSSL-{registry_version}-OpenSSLProject' + - 'SOFTWARE\Wow6432Node\OpenSSL-{registry_version}-OpenSSLProject' values: # Expected REG_SZ values under each path. Paths are compared # case-insensitively on the drive letter and exactly on the rest. @@ -26,20 +34,22 @@ registry: # Expected files. Keys are directories relative to {install_dir}; "" is the root. # Each entry's `flags` indicates which install configurations include it: # all, app, sdk, fips, fips_sdk -# Placeholders in `name`: {major}, {minor}, {patch} +# Placeholders in `name`: {major}, {minor}, {patch}, {arch} ("x64", "arm64" or +# "x86", taken from the installer filename) and {dll_suffix} (OpenSSL's DLL +# name suffix for that arch: "-x64", "-arm64", or "" for x86). files: "": - { name: LICENSE.txt, flags: all } - { name: version.dat, flags: all } "bin": - - { name: openssl.exe, flags: app } - - { name: "libcrypto-{major}-x64.dll", flags: app } - - { name: "libssl-{major}-x64.dll", flags: app } + - { name: openssl.exe, flags: app } + - { name: "libcrypto-{major}{dll_suffix}.dll", flags: app } + - { name: "libssl-{major}{dll_suffix}.dll", flags: app } "lib": - - { name: "libcrypto-{major}-x64.dll", flags: sdk } - - { name: "libssl-{major}-x64.dll", flags: sdk } - - { name: "libcrypto-{major}-x64.pdb", flags: sdk } - - { name: "libssl-{major}-x64.pdb", flags: sdk } + - { name: "libcrypto-{major}{dll_suffix}.dll", flags: sdk } + - { name: "libssl-{major}{dll_suffix}.dll", flags: sdk } + - { name: "libcrypto-{major}{dll_suffix}.pdb", flags: sdk } + - { name: "libssl-{major}{dll_suffix}.pdb", flags: sdk } - { name: libcrypto.lib, flags: sdk } - { name: libssl.lib, flags: sdk } - { name: libcrypto_static.lib, flags: sdk } diff --git a/tests/conftest.py b/tests/conftest.py index 0c875c0..3f9b510 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -165,6 +165,8 @@ class InstallerInfo: patch: str # "0" short: str # "4.0" — also the registry_version flavor: str # CRT flavor: "vs" (VC-WIN64A) or "hybrid" (VC-WIN64A-HYBRIDCRT) + arch: str # target architecture as spelled in the artifact name: "x64", "arm64" or "x86" + dll_suffix: str # OpenSSL's multilib DLL name suffix for that arch: "-x64", "-arm64" or "" (x86) @pytest.fixture(scope="session") @@ -279,6 +281,35 @@ def _detect_flavor(filename: str) -> str: ) +def _detect_arch(filename: str) -> str: + """Derive the target architecture from the installer filename. + + Artifacts are named OpenSSL---.{exe,msi} with + being "x64" (VC-WIN64A[-HYBRIDCRT]), "arm64" (VC-WIN64-ARM-HYBRIDCRT) or "x86" + (VC-WIN32[-HYBRIDCRT]). + """ + m = re.search(r"-(x64|arm64|x86)-", filename, re.IGNORECASE) + if m: + return m.group(1).lower() + pytest.exit( + f"Cannot determine architecture (expected '-x64-', '-arm64-' or '-x86-') from installer filename: {filename}", + returncode=2, + ) + + +def _dll_suffix(arch: str) -> str: + """OpenSSL's `multilib` DLL name suffix: libcrypto--x64.dll and + libcrypto--arm64.dll, but plain libcrypto-.dll for 32-bit.""" + return "" if arch == "x86" else f"-{arch}" + + +def install_root(config: dict, info: InstallerInfo) -> Path: + """Per-architecture install root: 32-bit packages land in the 32-bit + Program Files (paths.install_root_x86), everything else in paths.install_root.""" + key = "install_root_x86" if info.arch == "x86" else "install_root" + return Path(config["paths"][key]) + + @pytest.fixture(scope="session") def installer(request, tmp_path_factory) -> InstallerInfo: arg = request.config.getoption("--installer") @@ -301,6 +332,7 @@ def installer(request, tmp_path_factory) -> InstallerInfo: version = m.group(1) major, minor, patch = version.split(".") flavor = _detect_flavor(path.name) + arch = _detect_arch(path.name) # Isolation: copy ONLY this installer into an otherwise-empty directory and # test that copy. A non-self-contained .exe bootstrapper co-located with a @@ -321,12 +353,14 @@ def installer(request, tmp_path_factory) -> InstallerInfo: patch=patch, short=f"{major}.{minor}", flavor=flavor, + arch=arch, + dll_suffix=_dll_suffix(arch), ) @pytest.fixture(scope="session") def install_dir(installer, config) -> Path: - return Path(config["paths"]["install_root"]) / f"openssl-{installer.short}" + return install_root(config, installer) / f"openssl-{installer.short}" @pytest.fixture(scope="session", autouse=True) @@ -414,26 +448,76 @@ def clean_install(installer): def _msiexec(args: list[str], check: bool) -> subprocess.CompletedProcess: - return subprocess.run( - ["msiexec", *args], - check=check, + """Run msiexec with a verbose log. msiexec itself is silent, so on a + non-zero exit the log tail is printed (pytest shows it as captured stdout + of the failing test) — it is the only place Windows Installer explains + codes like 1620 (package could not be opened) or 1633 (unsupported + platform). Raises CalledProcessError when `check` is set, like before.""" + log_path = _mkstemp_path("msiexec-", ".log") + res = subprocess.run( + ["msiexec", *args, "/l*v", str(log_path)], + check=False, capture_output=True, text=True, ) + if res.returncode != 0: + print(f"msiexec {' '.join(args)} exited with {res.returncode}; log tail ({log_path}):", flush=True) + print(_msiexec_log_tail(log_path), flush=True) + if check: + raise subprocess.CalledProcessError(res.returncode, res.args, output=res.stdout, stderr=res.stderr) + else: + log_path.unlink(missing_ok=True) + return res + + +def _msiexec_log_tail(log_path: Path, lines: int = 40) -> str: + """Verbose msiexec logs are UTF-16 with BOM on current Windows, ANSI on older ones.""" + try: + raw = log_path.read_bytes() + except OSError as e: + return f"" + if not raw: + return "" + text = raw.decode("utf-16") if raw.startswith((b"\xff\xfe", b"\xfe\xff")) else raw.decode("mbcs", errors="replace") + return "\n".join(text.splitlines()[-lines:]) def install(info: InstallerInfo, properties: list[str] | None = None, check: bool = True): props = properties or [] if info.path.suffix.lower() == ".exe": - return subprocess.run( - [str(info.path), "/exenoui", "/qn", *props], - check=check, + # Two logs: /exelog is the Advanced Installer bootstrapper's own log + # (prerequisite checks, extraction, how it launched the MSI); /l*v is + # passed through to the inner MSI like any msiexec option. Both tails are + # printed on failure, since the bootstrapper otherwise just returns + # msiexec's exit code (or -1 when a prerequisite is declined). + exe_log = _mkstemp_path("exe-bootstrapper-", ".log") + msi_log = _mkstemp_path("exe-msi-", ".log") + res = subprocess.run( + [str(info.path), "/exenoui", "/exelog", str(exe_log), "/qn", "/l*v", str(msi_log), *props], + check=False, capture_output=True, text=True, ) + if res.returncode != 0: + print(f"{info.path.name} {' '.join(props)} exited with {res.returncode}", flush=True) + for label, log in (("bootstrapper log", exe_log), ("inner MSI log", msi_log)): + print(f"--- {label} tail ({log}):", flush=True) + print(_msiexec_log_tail(log), flush=True) + if check: + raise subprocess.CalledProcessError(res.returncode, res.args, output=res.stdout, stderr=res.stderr) + else: + exe_log.unlink(missing_ok=True) + msi_log.unlink(missing_ok=True) + return res return _msiexec(["/i", str(info.path), "/qn", *props], check=check) +def _mkstemp_path(prefix: str, suffix: str) -> Path: + fd, name = tempfile.mkstemp(prefix=prefix, suffix=suffix) + os.close(fd) + return Path(name) + + def uninstall(info: InstallerInfo) -> None: """Best-effort uninstall via product-code lookup. OK if nothing is installed.""" _uninstall_all_openssl_products() @@ -461,12 +545,12 @@ def supported_fips_type(info: InstallerInfo) -> str: def _expand(name: str, info: InstallerInfo) -> str: - return name.format(major=info.major, minor=info.minor, patch=info.patch) + return name.format(major=info.major, minor=info.minor, patch=info.patch, arch=info.arch, dll_suffix=info.dll_suffix) def expected_files(config: dict, info: InstallerInfo, active_flags: tuple[str, ...]) -> tuple[list[Path], list[Path]]: """Return (should-exist, should-not-exist) absolute file paths.""" - root = Path(config["paths"]["install_root"]) / f"openssl-{info.short}" + root = install_root(config, info) / f"openssl-{info.short}" flags = set(active_flags) | {"all"} yes: list[Path] = [] no: list[Path] = [] @@ -615,7 +699,10 @@ def check_registry(config: dict, info: InstallerInfo, install_dir: Path): "install_dir": str(install_dir).rstrip("\\"), } expected_values = {k: v.format(**fmt) for k, v in config["registry"]["values"].items()} - for path_template in config["registry"]["paths"]: + # 32-bit packages have their HKLM\SOFTWARE writes redirected into Wow6432Node, + # so the expected key paths differ (registry.paths_x86). + path_templates = config["registry"]["paths_x86"] if info.arch == "x86" else config["registry"]["paths"] + for path_template in path_templates: path = path_template.format(**fmt) try: key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, path) @@ -676,8 +763,15 @@ def imported_dlls(binary: Path) -> set[str]: # win32com: MSI's parameterized StringData property is unreachable through # win32com's dynamic dispatch (it invokes the property-get as a method). -HYBRID_BUILD_NAMES = ("ExeBuild_hybrid", "MsiBuild_hybrid") -VS_BUILD_NAMES = ("ExeBuild", "MsiBuild") +HYBRID_BUILD_NAMES = ( + "ExeBuild_hybrid", + "MsiBuild_hybrid", + "ExeBuild_arm64_hybrid", + "MsiBuild_arm64_hybrid", + "ExeBuild_x86_hybrid", + "MsiBuild_x86_hybrid", +) +VS_BUILD_NAMES = ("ExeBuild", "MsiBuild", "ExeBuild_x86", "MsiBuild_x86") _MSI_QUERY_SCRIPT = Path(__file__).parent / "msi_query.ps1" diff --git a/tests/test_lifecycle.py b/tests/test_lifecycle.py index 63b787e..b53de22 100644 --- a/tests/test_lifecycle.py +++ b/tests/test_lifecycle.py @@ -94,43 +94,75 @@ def test_upgrade_from_previous_version(installer: InstallerInfo) -> None: # Minimum VC++ runtime version the .aip's PreReqSearch enforces. If the # installer's prereq mechanism works, this version (or newer) is on the # machine after install — either because it was already there or because -# the MSI downloaded https://aka.ms/vs/17/release/vc_redist.x64.exe and -# installed it silently. -_VCRUNTIME_KEY = r"SOFTWARE\Microsoft\DevDiv\VC\Servicing\14.0\RuntimeMinimum" +# the MSI installed its bundled VC_redist..exe silently. _VCRUNTIME_MIN = (14, 40, 33816) +# Where the VC++ 2015-2022 runtime records its version, per architecture, as +# (registry view, key). Microsoft's documented detection key is +# SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\ (Version = "v14.x.y.z"), +# written for x86/x64/arm64 and read through the 32-bit view (Wow6432Node); the +# native view is tried too. The DevDiv servicing key is what the x64 .aip +# prerequisite search uses, and is only known to exist for x86/x64 runtimes +# (the arm64 runtime does not write it), so it stays as a fallback. +_VCRUNTIME_RUNTIMES_KEY = r"SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes" +_VCRUNTIME_DEVDIV_KEY = r"SOFTWARE\Microsoft\DevDiv\VC\Servicing\14.0\RuntimeMinimum" + + +def _vc_runtime_keys(arch: str) -> list[tuple[int, str]]: + runtimes = rf"{_VCRUNTIME_RUNTIMES_KEY}\{arch}" + devdiv_view = winreg.KEY_WOW64_32KEY if arch == "x86" else winreg.KEY_WOW64_64KEY + return [ + (winreg.KEY_WOW64_32KEY, runtimes), + (winreg.KEY_WOW64_64KEY, runtimes), + (devdiv_view, _VCRUNTIME_DEVDIV_KEY), + ] + def _version_tuple(s: str, length: int) -> tuple[int, ...]: - parts = [int(p) for p in s.split(".")] + parts = [int(p) for p in s.lstrip("vV").split(".")] while len(parts) < length: parts.append(0) return tuple(parts[:length]) +# Hybrid builds link vcruntime statically and use the OS Universal CRT, so the +# .aip gives them no VC++ redistributable prerequisite to verify. +_HYBRID_NO_VC_RUNTIME = "hybridCRT installers do not depend on or bundle the VC++ runtime" + + @pytest.mark.usefixtures("clean_install") def test_vc_runtime_present_after_install(installer: InstallerInfo) -> None: - """After install, the VC++ 2015-2022 x64 runtime must satisfy the .aip's - declared minimum (>= 14.40.33816). The MSI either uses an already-installed - runtime or downloads + installs vc_redist.x64.exe during install.""" + """After install, the VC++ 2015-2022 runtime for the installer's + architecture must satisfy the .aip's declared minimum (>= 14.40.33816). + The MSI either uses an already-installed runtime or installs its bundled + VC_redist..exe during install.""" + if installer.flavor == "hybrid": + pytest.skip(_HYBRID_NO_VC_RUNTIME) install(installer) - _assert_vc_runtime_meets_minimum() + _assert_vc_runtime_meets_minimum(installer.arch) -def _read_vc_runtime_version() -> str | None: - """Return the VC++ runtime version string, or None if not installed.""" - try: - key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, _VCRUNTIME_KEY) - except FileNotFoundError: - return None - with key: - return winreg.QueryValueEx(key, "Version")[0] +def _read_vc_runtime_version(arch: str) -> str | None: + """Return the VC++ runtime version string for `arch`, or None if not installed.""" + for view, key_path in _vc_runtime_keys(arch): + try: + key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, key_path, 0, winreg.KEY_READ | view) + except FileNotFoundError: + continue + with key: + try: + return winreg.QueryValueEx(key, "Version")[0] + except FileNotFoundError: + continue + return None -def _assert_vc_runtime_meets_minimum() -> None: - version = _read_vc_runtime_version() +def _assert_vc_runtime_meets_minimum(arch: str) -> None: + version = _read_vc_runtime_version(arch) if version is None: + keys = ", ".join(f"HKLM\\{k}" for _, k in _vc_runtime_keys(arch)) raise AssertionError( - f"VC++ runtime registry key missing: HKLM\\{_VCRUNTIME_KEY}.\n" + f"VC++ {arch} runtime registry key missing (looked at {keys}).\n" "The MSI's prereq should have installed VC++ Redistributable." ) actual = _version_tuple(version, len(_VCRUNTIME_MIN)) @@ -138,16 +170,19 @@ def _assert_vc_runtime_meets_minimum() -> None: assert actual >= _VCRUNTIME_MIN, f"VC++ runtime version {version!r} < required {required_str!r}" -# Matches "Microsoft Visual C++ 2015/2017/2019/2022 (- ... -)? Redistributable (x64) ..." +# Matches "Microsoft Visual C++ 2015/2017/2019/2022 (- ... -)? Redistributable () ..." # in DisplayName. Older Visual C++ families (2008/2010/2012/2013) live on # different servicing branches and aren't what our installer requires, so # we leave them alone. -_VC_REDIST_X64_PATTERN = re.compile(r"visual c\+\+ 20(15|17|19|22).*x64", re.IGNORECASE) +def _vc_redist_pattern(arch: str) -> re.Pattern[str]: + return re.compile(r"visual c\+\+ 20(15|17|19|22).*" + re.escape(arch), re.IGNORECASE) -def _find_vc_redist_x64_products() -> list[tuple[str, str]]: - """Return [(product_code, display_name)] for installed VC++ 2015-2022 x64 - redistributables — the family our MSI's prereq targets.""" +def _find_vc_redist_products(arch: str) -> list[tuple[str, str]]: + """Return [(product_code, display_name)] for installed VC++ 2015-2022 + redistributables of the given architecture — the family our MSI's prereq + targets.""" + pattern = _vc_redist_pattern(arch) results: list[tuple[str, str]] = [] for hive_path in ( r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall", @@ -172,7 +207,7 @@ def _find_vc_redist_x64_products() -> list[tuple[str, str]]: display_name = winreg.QueryValueEx(subkey, "DisplayName")[0] except FileNotFoundError: continue - if _VC_REDIST_X64_PATTERN.search(display_name): + if pattern.search(display_name): results.append((subkey_name, display_name)) return results @@ -181,21 +216,23 @@ def _find_vc_redist_x64_products() -> list[tuple[str, str]]: @pytest.mark.usefixtures("clean_install") def test_msi_installs_vc_runtime_when_missing(installer: InstallerInfo) -> None: """Aggressive variant of test_vc_runtime_present_after_install: forcibly - remove every Visual C++ 2015-2022 x64 redistributable on the machine, - then install the MSI and verify the runtime is back at the required - version. + remove every Visual C++ 2015-2022 redistributable of the installer's + architecture from the machine, then install the MSI and verify the + runtime is back at the required version. - This proves the .aip's PreReqComponent actually downloads and installs - https://aka.ms/vs/17/release/vc_redist.x64.exe — not just relies on a - machine that happened to already have it. + This proves the .aip's PreReqComponent actually installs its bundled + VC_redist..exe — not just relies on a machine that happened to + already have it. Gated by the `destructive` marker (run with `pytest -m destructive`) because it temporarily breaks any other software on the machine that depends on VC++ runtime. The MSI's prereq mechanism restores it. """ - found = _find_vc_redist_x64_products() + if installer.flavor == "hybrid": + pytest.skip(_HYBRID_NO_VC_RUNTIME) + found = _find_vc_redist_products(installer.arch) if not found: - pytest.skip("no VC++ 2015-2022 x64 redistributable present to remove; cannot verify download") + pytest.skip(f"no VC++ 2015-2022 {installer.arch} redistributable present to remove; cannot verify install") for product_code, display_name in found: print(f"removing {display_name} ({product_code})", flush=True) @@ -210,7 +247,7 @@ def test_msi_installs_vc_runtime_when_missing(installer: InstallerInfo) -> None: # Confirm the runtime is genuinely absent (or below the minimum) before # we install our MSI — otherwise the test wouldn't prove anything. - version = _read_vc_runtime_version() + version = _read_vc_runtime_version(installer.arch) if version is not None: actual = _version_tuple(version, len(_VCRUNTIME_MIN)) if actual >= _VCRUNTIME_MIN: @@ -219,7 +256,7 @@ def test_msi_installs_vc_runtime_when_missing(installer: InstallerInfo) -> None: "system component or a newer redistributable kept it. Cannot verify the prereq download." ) - # Install our MSI. The PreReqComponent should download + install VC++ redist. + # Install our MSI. The PreReqComponent should install the bundled VC++ redist. install(installer) - _assert_vc_runtime_meets_minimum() + _assert_vc_runtime_meets_minimum(installer.arch) diff --git a/tests/test_silent_install.py b/tests/test_silent_install.py index 7e359a7..3adb2aa 100644 --- a/tests/test_silent_install.py +++ b/tests/test_silent_install.py @@ -194,11 +194,11 @@ def test_crt_flavor_import_table(installer: InstallerInfo, install_dir: Path) -> reflect the installer's flavor (the shared validated FIPS module is always VC-WIN64A and is intentionally excluded here).""" install(installer, ["INSTALL_APP=1", "INSTALL_SDK=1"]) - major = installer.major + major, suffix = installer.major, installer.dll_suffix binaries = [ install_dir / "bin" / "openssl.exe", - install_dir / "bin" / f"libcrypto-{major}-x64.dll", - install_dir / "bin" / f"libssl-{major}-x64.dll", + install_dir / "bin" / f"libcrypto-{major}{suffix}.dll", + install_dir / "bin" / f"libssl-{major}{suffix}.dll", install_dir / "lib" / "ossl-modules" / "legacy.dll", ] problems: list[str] = [] diff --git a/windows-installer/0001-Windows-Use-Z7-compiler-flag-to-enable-parallel-buil.patch b/windows-installer/0001-Windows-Use-Z7-compiler-flag-to-enable-parallel-buil.patch deleted file mode 100644 index e1c0c5c..0000000 --- a/windows-installer/0001-Windows-Use-Z7-compiler-flag-to-enable-parallel-buil.patch +++ /dev/null @@ -1,91 +0,0 @@ -From be67880c1e6e37102a8488f81a8263554fa97d53 Mon Sep 17 00:00:00 2001 -From: Milan Broz -Date: Thu, 2 Apr 2026 12:51:46 +0200 -Subject: [PATCH] Windows: Use /Z7 compiler flag to enable parallel builds -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit - -MSVC compilation on Windows cannot be reliably parallelized -with tools like jom (an nmake replacement) due to contention -on shared .pdb files used for debug info. Writes to a shared -.pdb must be serialized. - -The /FS compiler flag serializes concurrent compiler writes, -but does not resolve contention when the compiler and linker -access the same .pdb file. With shared .pdb files (e.g. app.pdb), -the makefile does not prevent races between the linker and -compilation of multiple targets. - -This can be resolved either by restructuring the makefile -to introduce sentinel dependencies that serialize the conflicting -steps, or by eliminating the shared .pdb entirely. - -This patch takes the latter approach: it replaces /Zi with /Z7, -which embeds debug info directly into each .obj file and avoids -any shared-file contention. /Z7 is supported by all MSVC versions. - -The linker-generated .pdb is unaffected. - -Side effects: object files are slightly larger, and all .pdb files -are now named after their target — the shared app.pdb, ossl_static.pdb, -and dso.pdb no longer exist. - -With this change, jom can be used to parallelize the build. - -Fixes: #9931 - -Signed-off-by: Milan Broz - -Reviewed-by: Neil Horman -Reviewed-by: Norbert Pocs -MergeDate: Mon Apr 13 08:46:20 2026 -(Merged from https://github.com/openssl/openssl/pull/30703) ---- - Configurations/10-main.conf | 6 +++--- - Configurations/windows-makefile.tmpl | 4 +--- - 2 files changed, 4 insertions(+), 6 deletions(-) - -diff --git a/Configurations/10-main.conf b/Configurations/10-main.conf -index 76cbf0ffa0..c7002eff39 100644 ---- a/Configurations/10-main.conf -+++ b/Configurations/10-main.conf -@@ -1541,10 +1541,10 @@ my %targets = ( - "UNICODE", "_UNICODE", - "_CRT_SECURE_NO_DEPRECATE", - "_WINSOCK_DEPRECATED_NO_WARNINGS"), -- lib_cflags => add("/Zi /Fdossl_static.pdb"), -+ lib_cflags => add("/Z7"), - lib_defines => add("L_ENDIAN"), -- dso_cflags => "/Zi /Fddso.pdb", -- bin_cflags => "/Zi /Fdapp.pdb", -+ dso_cflags => "/Z7", -+ bin_cflags => "/Z7", - # def_flag made to empty string so a .def file gets generated - shared_defflag => '', - shared_ldflag => "/dll", -diff --git a/Configurations/windows-makefile.tmpl b/Configurations/windows-makefile.tmpl -index a3c52ac19d..16fed4670d 100644 ---- a/Configurations/windows-makefile.tmpl -+++ b/Configurations/windows-makefile.tmpl -@@ -450,7 +450,7 @@ uninstall: {- "uninstall_docs" if !$disabled{docs}; -} uninstall_sw {- $disabled - - libclean: - "$(PERL)" -e "map { m/(.*)\.dll$$/; unlink glob """{.,apps,test,fuzz}/$$1.*"""; } @ARGV" $(SHLIBS) -- -del /Q /F $(LIBS) libcrypto.* libssl.* ossl_static.pdb -+ -del /Q /F $(LIBS) libcrypto.* libssl.* - - clean: libclean - {- join("\n\t", map { "-if exist $_ del /Q /F $_" } @HTMLDOCS1) || "\@rem" -} -@@ -545,8 +545,6 @@ install_dev: install_runtime_libs - "$(INSTALLTOP)\include\openssl" - @"$(PERL)" "$(SRCDIR)\util\mkdir-p.pl" "$(libdir)" - @"$(PERL)" "$(SRCDIR)\util\copy.pl" $(INSTALL_LIBS) "$(libdir)" -- @if "$(SHLIBS)"=="" \ -- "$(PERL)" "$(SRCDIR)\util\copy.pl" ossl_static.pdb "$(libdir)" - @"$(PERL)" "$(SRCDIR)\util\mkdir-p.pl" "$(CMAKECONFIGDIR)" - @"$(PERL)" "$(SRCDIR)\util\copy.pl" $(INSTALL_EXPORTERS_CMAKE) "$(CMAKECONFIGDIR)" - --- -2.54.0 - diff --git a/windows-installer/Prerequisites/Visual C++ Redistributable for Visual Studio 2015-2022/VC_redist.x86.exe b/windows-installer/Prerequisites/Visual C++ Redistributable for Visual Studio 2015-2022/VC_redist.x86.exe new file mode 100644 index 0000000..cc845c5 Binary files /dev/null and b/windows-installer/Prerequisites/Visual C++ Redistributable for Visual Studio 2015-2022/VC_redist.x86.exe differ diff --git a/windows-installer/README.md b/windows-installer/README.md index 842b059..0a1040d 100644 --- a/windows-installer/README.md +++ b/windows-installer/README.md @@ -12,16 +12,21 @@ The installer comes in two variants: EXE and MSI. ### Naming -* `OpenSSL-x64-VS-` — includes the Microsoft Visual Studio redistributable required for `openssl.exe` to function. +`` is `x64`, `x86` or `arm64`. + +* `OpenSSL--VS-` (x64, x86) — includes the Microsoft Visual Studio redistributable required for `openssl.exe` to function. The redistributable package is installed only if it is not already present on the machine. -* `OpenSSL-x64-hybridCRT-` — contains OpenSSL built using the Hybrid CRT method, so it does not depend on the Visual Studio redistributable. +* `OpenSSL--hybridCRT-` (x64, x86, arm64) — contains OpenSSL built using the Hybrid CRT method, so it does not depend on the Visual Studio redistributable. For more information, see [Hybrid CRT documentation](https://github.com/microsoft/WindowsAppSDK/blob/77761e244289fda6b3d5f14c7bded189fed4fb89/docs/Coding-Guidelines/HybridCRT.md). ## Supported Windows versions and platforms -The installer supports x64 platform builds only and can be run on Windows 7 / Windows Server 2008 R2 or more recent versions. -The Visual Studio redistributable is installed in its x64 version only. +* **x64 and x86** — Windows 7 / Windows Server 2008 R2 or more recent versions. + The x86 installer installs into `Program Files (x86)`. + The VS flavour installs the Visual Studio redistributable matching the installer's architecture. +* **arm64** — Windows 10, Windows 11 and Windows Server. Available in the Hybrid CRT flavour only. + For HybridCRT flavour to work on older versions than Windows 10, the Universal CRT has to be updated, [see](https://support.microsoft.com/en-us/servicing/os/windows/2020/06/update-for-universal-c-runtime-in-windows). ## Installation options diff --git a/windows-installer/openssl.aip b/windows-installer/openssl.aip index f08483c..9510237 100644 --- a/windows-installer/openssl.aip +++ b/windows-installer/openssl.aip @@ -3,37 +3,37 @@ - - + + - + - - + + - - - + + + - + - + - + @@ -45,18 +45,20 @@ - - - - - - - - - - - - + + + + + + + + + + + + + + @@ -106,6 +108,14 @@ + + + + + + + + @@ -139,14 +149,30 @@ + + + + + + + + + - + + + + + + + + @@ -165,16 +191,28 @@ - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + @@ -205,6 +243,12 @@ + + + + + + @@ -264,8 +308,8 @@ - - + + @@ -291,6 +335,12 @@ + + + + + + @@ -398,7 +448,7 @@ - + @@ -480,7 +530,7 @@ - + @@ -561,8 +611,8 @@ - - + + @@ -629,12 +679,21 @@ - - - + + + - + + + + + + + + + + @@ -665,16 +724,16 @@ - - - - - - - - - - + + + + + + + + + + @@ -696,13 +755,14 @@ - - - - - - - + + + + + + + + @@ -737,6 +797,7 @@ + @@ -766,10 +827,12 @@ - + + +