Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion .github/workflows/build-php.yml
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,14 @@ jobs:
build-windows-amd64:
name: Windows x64
needs: resolve-version
runs-on: windows-latest
# Not windows-latest or windows-2025: those images now ship Visual Studio
# 2026, and static-php-cli only looks for 2022 or 2019, so its doctor check
# fails before anything is built.
runs-on: windows-2022
steps:
- name: Check out repository code
uses: actions/checkout@v7

- name: Clone static-php-cli
run: |
git clone --depth 1 --branch ${{ env.SPC_VERSION }} https://github.com/crazywhalecc/static-php-cli.git spc
Expand All @@ -238,6 +244,12 @@ jobs:
cd spc
composer install --no-dev --classmap-authoritative

# Schannel cannot use a CA file and the Windows certificate store
# together, which the CLI needs. This must run before the download step,
# which reads the dependencies it changes.
- name: Build curl against OpenSSL, not Schannel
run: php scripts/patch-spc-windows-curl.php spc

- name: Setup build environment
run: |
cd spc
Expand All @@ -259,6 +271,18 @@ jobs:
$extensions = $extensions.Trim(',')
./bin/spc build "$extensions" --build-cli ${{ inputs.debug && '--debug' || '' }}

# The CLI depends on the SSL backend: only OpenSSL can use a CA file and
# the Windows certificate store together.
- name: Check the SSL backend
run: |
$curl = & ./spc/buildroot/bin/php.exe -n -r "echo curl_version()['ssl_version'];"
$openssl = & ./spc/buildroot/bin/php.exe -n -r "echo OPENSSL_VERSION_TEXT;"
Write-Host "curl: $curl"
Write-Host "openssl extension: $openssl"
if ($curl -notlike 'OpenSSL/*') {
throw "expected curl to be built against OpenSSL, got '$curl'"
}

- name: Upload artifact
uses: actions/upload-artifact@v7
with:
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,22 @@ The workflow will:
- **Linux/macOS**: static-php-cli automatically uses the system CA bundle
- **Windows**: Requires explicit `cacert.pem` configuration (handled by the CLI)

### Windows SSL backend

static-php-cli builds curl with Schannel on Windows. These builds change that to
OpenSSL, which is already built for the `openssl` extension, using
`scripts/patch-spc-windows-curl.php`.

Schannel verifies against a CA file *instead of* the Windows certificate store,
and refuses a file larger than 1 MiB. The CLI has to pass a CA file, because the
`openssl` extension cannot read the store at all, so with Schannel a root
certificate installed by an organization is never trusted. curl built against
OpenSSL loads a CA file and the Windows stores together, and has no size limit.

The patch fails the build if static-php-cli changes in a way it does not expect,
rather than quietly producing a Schannel build. See
[upsun/cli#110](https://github.com/upsun/cli/issues/110).

## License

The build scripts in this repository are MIT licensed. PHP binaries are subject to the [PHP License](https://www.php.net/license/).
107 changes: 107 additions & 0 deletions scripts/patch-spc-windows-curl.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?php

/**
* Builds curl against OpenSSL instead of Schannel, on Windows.
*
* static-php-cli builds curl with Schannel. Schannel cannot combine a CA file
* with the Windows certificate store: given a file it verifies against that
* file alone, so an organization's own root certificate, which is installed in
* the store, is not seen. It also refuses a CA file larger than 1 MiB, which a
* bundle including the store's certificates can exceed. The CLI needs a CA
* file, because the openssl extension cannot read the store at all, so with
* Schannel there is no way to trust both sets of certificates.
*
* curl built against OpenSSL loads a CA file and the Windows stores together,
* and has no size limit. OpenSSL is already built here for the openssl
* extension.
*
* See https://github.com/upsun/cli/issues/110, and
* https://github.com/crazywhalecc/static-php-cli/pull/674 which chose Schannel.
*
* Usage: php scripts/patch-spc-windows-curl.php [path to static-php-cli]
*/

declare(strict_types=1);

$spcDir = $argv[1] ?? 'spc';

/**
* Stops with a message, so that a build never quietly uses the wrong backend.
*/
function fail(string $message): never
{
fwrite(STDERR, 'patch-spc-windows-curl: ' . $message . PHP_EOL);
exit(1);
}

function readFileOrFail(string $path): string
{
if (!is_file($path)) {
fail("$path does not exist: check the static-php-cli version");
}
$contents = file_get_contents($path);
if ($contents === false) {
fail("could not read $path");
}
return $contents;
}

function writeFileOrFail(string $path, string $contents): void
{
if (file_put_contents($path, $contents) === false) {
fail("could not write $path");
}
}

// curl needs the OpenSSL library on Windows. static-php-cli removed it from
// curl's dependencies when it switched to Schannel, and without it the build
// order does not guarantee OpenSSL is there first.
$libFile = $spcDir . '/config/lib.json';
$libs = json_decode(readFileOrFail($libFile), true);
if (!is_array($libs) || !isset($libs['curl']['lib-depends-windows']) || !is_array($libs['curl']['lib-depends-windows'])) {
fail("$libFile does not list curl's Windows dependencies: check the static-php-cli version");
}
if (in_array('openssl', $libs['curl']['lib-depends-windows'], true)) {
echo "curl already depends on openssl on Windows\n";
} else {
array_unshift($libs['curl']['lib-depends-windows'], 'openssl');
writeFileOrFail($libFile, json_encode($libs, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n");
echo "added openssl to curl's Windows dependencies\n";
}

// Choose the backend in the cmake options curl is built with.
$curlFile = $spcDir . '/src/SPC/builder/windows/library/curl.php';
$source = readFileOrFail($curlFile);
$options = [
"'-DUSE_WINDOWS_SSPI=ON '" => "'-DUSE_WINDOWS_SSPI=OFF '",
"'-DCURL_USE_SCHANNEL=ON '" => "'-DCURL_USE_SCHANNEL=OFF '",
"'-DCURL_USE_OPENSSL=OFF '" => "'-DCURL_USE_OPENSSL=ON '",
];
foreach ($options as $from => $to) {
if (substr_count($source, $to) === 1 && !str_contains($source, $from)) {
echo "already set: $to\n";
continue;
}
if (substr_count($source, $from) !== 1) {
fail("expected exactly one $from in $curlFile: check the static-php-cli version");
}
$source = str_replace($from, $to, $source);
echo "set $to\n";
}

// Use the Windows certificate store when no CA file is set, which is what
// Schannel did. Only then: given a CA file, curl uses that alone unless asked
// for both. This keeps verification working if the CLI cannot write its bundle.
$nativeCA = "'-DCURL_CA_NATIVE=ON '";
$anchor = "'-DCURL_ENABLE_SSL=ON ' .";
if (str_contains($source, $nativeCA)) {
echo "already set: $nativeCA\n";
} else {
if (substr_count($source, $anchor) !== 1) {
fail("expected exactly one $anchor in $curlFile: check the static-php-cli version");
}
$source = str_replace($anchor, $nativeCA . " .\n " . $anchor, $source);
echo "set $nativeCA\n";
}

writeFileOrFail($curlFile, $source);
Loading