Build APM on Windows, Part 2: Fetch Source and Libs with vcpkg
Before compiling anything, you need source files checked for tampering and every lib Apache requires. These two jobs take almost an hour combined on the first run, but you only do them once. On subsequent runs, almost everything comes from the cache.
I needed to write these instructions so someone who had never compiled anything could follow them, so I opened both scripts and read them again line by line, then checked every command in this article against the machine where I had already built the stack. Part 2 is the one I want you to read most slowly, because it determines where the material compiled into your web server comes from and how much you can trust it. If the downloaded source has been tampered with, even if you follow every line of the later steps correctly, the result is Apache with something extra inside that you do not know about.
This part covers two scripts: 00-fetch.ps1 downloads and verifies the hashes of all source archives and tools, and 10-deps-vcpkg.ps1 has vcpkg compile the libs Apache needs (OpenSSL, PCRE2, zlib, nghttp2, brotli, expat, libxml2, jansson, curl) from source using MSVC. If you have not read Part 1, installed Build Tools, and extracted the kit to C:\apm-build, go back and do that first, because this part assumes all the tools are ready.
PHP released patches for two critical RCE vulnerabilities on 2026-10-21 (details are in the php2shell news article), so I built my own stack so I could change the version numbers in a single versions.json file and rebuild on the same day upstream releases an update. Part 2 is what makes that possible, because every source archive has its hash checked before use.
1. Preparation: Check That Part 1 Is Complete
Every step in this part and the following parts runs in PowerShell 7 (pwsh) as a regular user, without opening it as Administrator. The build scripts load the MSVC environment themselves, so you do not need to open a Developer Prompt. Section 1 is a check before starting; work through each step in order.
Step 1.1 Open pwsh
Where: Click Start, type pwsh, and open regular PowerShell 7 (not Run as administrator). The window title should say PowerShell 7, not Windows PowerShell.
$PSVersionTable.PSVersion.ToString()
You should see version 7.5 or later (my machine reports 7.6.7). If you get 5.1, you opened the wrong one. The 00-fetch.ps1 script uses ConvertFrom-Json -AsHashtable, which is available only in PowerShell 6 and later; running it in 5.1 will fail immediately.
Step 1.2 Go to the kit folder
Set-Location C:\apm-build
Get-ChildItem -Name
You should see this listing (the order may differ slightly).
deps
ports-overlay
scripts
templates
tools
.gitignore
build.cmd
deploy.cmd
README.txt
versions.json
versions.lock.json
If you do not see build.cmd here, the zip was extracted into a nested folder (C:\apm-build\apm-build-kit\build.cmd). Move everything up one level as described in Part 1 before continuing.
Step 1.3 Check that the main tools respond
git --version; cmake --version | Select-Object -First 1; ninja --version; pwsh --version
git version 2.54.0.windows.1
cmake version 4.1.2
1.13.1
PowerShell 7.6.1
Your versions may be newer than these; all that matters is that every tool responds. If any of them reports is not recognized, open a new pwsh window (winget has just added entries to PATH), and if it still does not work, go back to Part 1.
Step 1.4 Check disk space
[math]::Round((Get-PSDrive C).Free / 1GB, 1)
The number must be at least 60 (in GB), because building the entire series uses about 45 GB. Part 2 does not use much, but running out of disk space halfway through Part 5 or 6 will waste much more time than checking now.
2. 00-fetch.ps1: Download and Verify Hashes Before Extracting
This script has one job: read versions.json, fetch every entry with a url into downloads\, verify its sha256, and extract it into toolchain\ (tools) or src\ (source). Here is the full script; read through it once before looking at the explanations of each section below.
# 00-fetch.ps1 - download + verify + extract everything pinned in versions.json
# Usage: pwsh scripts\00-fetch.ps1 [-Only php,httpd] [-Force]
param([string[]]$Only, [switch]$Force)
$ErrorActionPreference = 'Stop'
# `pwsh -File ... -Only php,httpd` passes ONE string "php,httpd" (no array parsing in -File mode)
$Only = @($Only | ForEach-Object { $_ -split ',' } | ForEach-Object { $_.Trim() } | Where-Object { $_ })
$Root = Split-Path $PSScriptRoot -Parent
$V = Get-Content "$Root\versions.json" -Raw | ConvertFrom-Json
$LockFile = "$Root\versions.lock.json"
$Lock = if (Test-Path $LockFile) { Get-Content $LockFile -Raw | ConvertFrom-Json -AsHashtable } else { @{} }
$Dl = "$Root\downloads"; New-Item -ItemType Directory -Force $Dl, "$Root\src" | Out-Null
function Get-One($name, $item, $destDir, $strip) {
if ($Only -and $name -notin $Only) { return }
if (-not $item.url) { return } # git-based sources (imagemagick) are fetched by their own build script
$file = Join-Path $Dl ([IO.Path]::GetFileName(([uri]$item.url).AbsolutePath))
if (-not (Test-Path $file) -or $Force) {
Write-Host "[GET] $name $($item.version)"
curl.exe -fsSL --retry 3 -o "$file.part" $item.url
if ($LASTEXITCODE) { throw "download failed: $($item.url)" }
Move-Item -Force "$file.part" $file
}
$sha = (Get-FileHash $file -Algorithm SHA256).Hash.ToLower()
$want = if ($item.sha256) { $item.sha256 } elseif ($Lock[$name]) { $Lock[$name] } else { $null }
if ($want -and $sha -ne $want) { throw "[ERR] sha256 mismatch $name`n got $sha`n want $want" }
if ($item.sha1) {
$s1 = (Get-FileHash $file -Algorithm SHA1).Hash.ToLower()
if ($s1 -ne $item.sha1) { throw "[ERR] sha1 mismatch $name" }
}
if (-not $want) { Write-Host "[TOFU] $name sha256=$sha" }
$Lock[$name] = $sha
if ((Test-Path $destDir) -and -not $Force) { Write-Host "[OK] $name (already extracted)"; return }
if (Test-Path $destDir) { Remove-Item $destDir -Recurse -Force }
New-Item -ItemType Directory -Force $destDir | Out-Null
tar.exe -xf $file -C $destDir --strip-components=$strip
if ($LASTEXITCODE) { throw "extract failed: $file" }
Write-Host "[OK] $name -> $destDir"
}
foreach ($p in $V.toolchain.PSObject.Properties) {
Get-One $p.Name $p.Value (Join-Path $Root $p.Value.dest) $p.Value.strip
}
foreach ($p in $V.sources.PSObject.Properties) {
Get-One $p.Name $p.Value (Join-Path "$Root\src" "$($p.Name)-$($p.Value.version)") 1
}
$Lock | ConvertTo-Json | Set-Content $LockFile -Encoding utf8
The versions.json file the script reads
I have shortened the hashes for readability. The actual file in the kit contains full 64-character hashes.
{
"apm_version": "1.0.0",
"toolchain": {
"php-sdk": { "version": "2.8.4", "url": "https://github.com/php/php-sdk-binary-tools/archive/refs/tags/php-sdk-2.8.4.zip", "sha256": null, "dest": "toolchain/php-sdk", "strip": 1 },
"nasm": { "version": "3.01", "url": "https://www.nasm.us/pub/nasm/releasebuilds/3.01/win64/nasm-3.01-win64.zip", "sha256": null, "dest": "toolchain/nasm", "strip": 1 }
},
"sources": {
"php": { "version": "8.4.27", "url": "https://www.php.net/distributions/php-8.4.27.tar.xz", "sha256": "dcf93923b0e38e0d…" },
"httpd": { "version": "2.4.69", "url": "https://downloads.apache.org/httpd/httpd-2.4.69.tar.bz2", "sha256": "c5e6ebc66e349b87…" },
"mod_fcgid": { "version": "2.3.9", "url": "https://archive.apache.org/dist/httpd/mod_fcgid/mod_fcgid-2.3.9.tar.bz2", "sha256": null, "sha1": "f0b6d87dfcfe18b3…" },
"imagemagick": { "version": "7.1.2-34", "url": null, "sha256": null }
}
}
The actual file has 4 toolchain entries (php-sdk, strawberry, nasm, winflexbison) and 10 sources entries (php, httpd, apr, apr-util, mod_fcgid, mariadb, pecl-redis, pecl-igbinary, pecl-imagick, imagemagick). There are three things to know before reading the script.
"sha256": nulldoes not mean verification is skipped. It means upstream has not published a hash, so the script should useversions.lock.jsoninstead (explained in the next section).stripis the number of folder levels removed during extraction. Most tarballs contain an outer folder named something likehttpd-2.4.69\. I strip it off (stripis 1) to getsrc\httpd-2.4.69\CMakeLists.txtdirectly, without two nested levels.imagemagickhas aurlofnullbecause it is not a file to download, but a git tag that Part 5 clones separately. When the script finds an empty url, it silently skips it without printing anything.
Read Get-One in execution order
- Filter by
-OnlyIf you specify-Only php, every name other thanphpis skipped immediately. (Before this point, the$Only = @(...)line at the top has already splitphp,httpdinto two names.) - Derive the filename from the URL Use the last component of the URL path as the filename in
downloads\, such ashttpd-2.4.69.tar.bz2. - Download if the file is missing or
-Forceis specified Use thecurl.exebundled with Windows (-ffails on HTTP errors,-sSstays quiet but shows errors,-Lfollows redirects,--retry 3retries three times). Download to a file ending in.partfirst, then rename it withMove-Itemonce the download finishes. If the connection drops midway, only a.partfile remains, rather than a partial file that looks complete. - Always calculate the file's sha256 Even if the file already exists, so files left from a previous run are checked again every time.
- Choose the comparison value (
$want) Ifversions.jsoncontains a sha256, use it. If it isnull, look inversions.lock.json. If neither file has one, there is nothing to compare against. - Mismatch = stop immediately Use
throwand print the actual value (got) and the expected value (want). The script never extracts a file whose hash does not match. - Also verify sha1 Only for entries that specify
sha1. There is just one:mod_fcgid, because the Apache project publishes a sha1 for it. - If there is nothing to compare against, print
[TOFU]Then record the calculated hash in$Lock. - Skip extraction if the destination folder already exists (Print
already extracted), unless-Forceis specified, in which case delete the folder and extract again. There is a trap here, explained under "If Something Goes Wrong Here." - Extract using
tar.exeBundled with Windows 10 and later (bsdtar), it can read.zip,.tar.bz2,.tar.xz, and.tgzfiles without installing 7-Zip. - The last line of the script Writes
$Lockback toversions.lock.jsonone final time.
3. How Do Upstream sha256 Hashes Differ from TOFU?
These two terms are central to the trustworthiness of the whole series, so I will explain them separately.
Case 1: Upstream publishes its own hash PHP, httpd, APR, APR-util, and MariaDB publish sha256 hashes on their release pages (php.net has JSON, Apache has .sha256 files alongside the tarballs, and MariaDB has a REST API). I have copied those values into versions.json myself. If someone swaps the file in transit or at a mirror, the hash will not match, and the script stops.
Case 2: TOFU (Trust On First Use) Some upstream projects do not publish hashes, such as GitHub releases (php-sdk, winflexbison, Strawberry Perl) and all three PECL packages. The available approach is to download the file once and record its hash, then compare subsequent downloads against that record. It cannot protect against the first file having been swapped, but it can detect a previously good file changing later.
The kit you downloaded includes versions.lock.json. I recorded each hash from the first download on my machine. As a result, your machine will not show any [TOFU] lines on its first run, because every entry already has a value in the lock to compare against, and the hashes for php-sdk, Strawberry Perl, NASM, winflexbison, mod_fcgid, and all three PECL packages will be compared against what I downloaded, rather than trusting the first file encountered. If you want to see TOFU actually work, delete versions.lock.json and run again; you will see [TOFU] <ชื่อ> sha256=<64 ตัวอักษร> instead.
Comparing against the lock only tells you that the file matches the one I previously received. It does not guarantee that the file's upstream source is safe. If you change the version of a TOFU entry (Part 7 explains the procedure), check the upstream release page first to confirm that the tag and file match the announcement, then let the script record the new hash.
4. Run fetch Step by Step
Step 4.1 Run the script
Where: pwsh (regular user), in C:\apm-build. Use the same pwsh -File form that build.cmd uses, which gets past the ExecutionPolicy issue with files downloaded from the internet (files in a zip often carry a "from the internet" marker that causes the RemoteSigned policy to refuse to run them).
pwsh -NoProfile -ExecutionPolicy Bypass -File scripts\00-fetch.ps1
Step 4.2 Watch the output while it runs
The first run takes 3-5 minutes, depending on your connection speed. The largest files are Strawberry Perl (about 290 MB) and MariaDB (about 211 MB). You will see pairs of lines, [GET] followed by [OK], for each entry. I have omitted some entries here.
[GET] php-sdk 2.8.4
[OK] php-sdk -> C:\apm-build\toolchain\php-sdk
[GET] strawberry 5.42.3.1
[OK] strawberry -> C:\apm-build\toolchain\strawberry
...
[GET] php 8.4.27
[OK] php -> C:\apm-build\src\php-8.4.27
[GET] httpd 2.4.69
[OK] httpd -> C:\apm-build\src\httpd-2.4.69
...
[OK] pecl-imagick -> C:\apm-build\src\pecl-imagick-3.8.1
The last line you should see is pecl-imagick. There is no line for imagemagick because it is skipped as explained earlier. If you see [ERR], jump to "If Something Goes Wrong Here."
Step 4.3 Check that all 13 files are present
Get-ChildItem C:\apm-build\downloads | Select-Object Name, @{n='MB';e={[math]::Round($_.Length/1MB,1)}}
You should get 13 rows, looking roughly like this (sizes from my machine).
Name MB
---- --
apr-1.7.6.tar.bz2 0.9
apr-util-1.6.5.tar.bz2 0.4
httpd-2.4.69.tar.bz2 8.1
igbinary-3.2.16.tgz 0.1
imagick-3.8.1.tgz 0.3
mariadb-11.8.9.tar.gz 211.4
mod_fcgid-2.3.9.tar.bz2 0.1
nasm-3.01-win64.zip 0.6
php-8.4.27.tar.xz 13.2
php-sdk-2.8.4.zip 34.4
redis-6.3.0.tgz 0.4
strawberry-perl-5.42.3.1-64bit-portable.zip 290.6
win_flex_bison-2.5.25.zip 1.0
Step 4.4 Check that the source was extracted
Get-ChildItem C:\apm-build\src -Name
Get-ChildItem C:\apm-build\toolchain -Name
apr-1.7.6
apr-util-1.6.5
httpd-2.4.69
mariadb-11.8.9
mod_fcgid-2.3.9
pecl-igbinary-3.2.16
pecl-imagick-3.8.1
pecl-redis-6.3.0
php-8.4.27
nasm
php-sdk
strawberry
winflexbison
There are 9 folders under src and 4 under toolchain (vcpkg is not there yet; it will appear in Section 7, and imagemagick-windows will appear in Part 5).
Step 4.5 Check that extraction used the correct folder level
This checks that strip worked. If extraction produced two nested levels, these files will not be found, and Part 3 will fail with a confusing error.
Test-Path C:\apm-build\src\httpd-2.4.69\CMakeLists.txt
Test-Path C:\apm-build\src\apr-1.7.6\CMakeLists.txt
Test-Path C:\apm-build\src\apr-util-1.6.5\CMakeLists.txt
Test-Path C:\apm-build\src\mod_fcgid-2.3.9\modules\fcgid\CMakeLists.txt
All four lines must return True.
Step 4.6 Check that the downloaded tools run
I check these two because vcpkg and OpenSSL depend on them later in the build, and using the wrong ones causes the most trouble.
C:\apm-build\toolchain\strawberry\perl\bin\perl.exe -e 'print "$^V $^O\n"'
C:\apm-build\toolchain\nasm\nasm.exe -v
v5.42.3 MSWin32
NASM version 3.01 compiled on Oct 10 2025
The word MSWin32 matters a great deal, because the perl bundled with Git for Windows is the cygwin variant (it reports cygwin instead), which generates Unix paths and breaks the OpenSSL build. That is why you must use this downloaded Strawberry Perl.
Step 4.7 Run it again to check that it skips work
pwsh -NoProfile -ExecutionPolicy Bypass -File scripts\00-fetch.ps1
This time it finishes in a few seconds (most of the time is spent recalculating hashes), because all the files are present. The script just recalculates the hashes and prints that the files have already been extracted. Here is the result (from my actual log).
[OK] php-sdk (already extracted)
[OK] strawberry (already extracted)
[OK] nasm (already extracted)
[OK] winflexbison (already extracted)
[OK] php (already extracted)
...
[OK] pecl-imagick (already extracted)
Step 4.8 (Optional) Check a hash manually against upstream
If you would rather not trust my script, compare it yourself. Here is an example using httpd.
Get-FileHash C:\apm-build\downloads\httpd-2.4.69.tar.bz2 -Algorithm SHA256 | Select-Object -ExpandProperty Hash
Then open https://downloads.apache.org/httpd/httpd-2.4.69.tar.bz2.sha256 in your browser and compare. The values must match (PowerShell prints uppercase, while Apache's file uses lowercase; ignore that difference). Mine starts with C5E6EBC66E34.
Use -Only and -Force to download selected entries
Part 7 will use these options when updating versions. You can pass several names at once straight through -File, separated by a comma.
pwsh -NoProfile -ExecutionPolicy Bypass -File scripts�-fetch.ps1 -Only php,httpd
This works because of the line added under $ErrorActionPreference (the one starting $Only = @(...)). pwsh -File does not turn php,httpd into an array. It passes one single string, "php,httpd", so $Only has one element and the name httpd matches nothing. The script then skips every entry silently, with no error. The current script splits the string itself before using it: it runs every value in $Only through -split ',', trims the ends with .Trim(), drops empty values with Where-Object { $_ }, and wraps the result in @(...) so it is always an array. So -Only php, -Only php,httpd and -Only "php, httpd" all behave the same.
To check it split correctly, run it and look for [OK] or [GET] lines for both names (if the files and folders already exist you get [OK] php (already extracted) and [OK] httpd (already extracted)). Older kits needed -Command "& .\scripts�-fetch.ps1 -Only php,httpd" instead, because they did not split the string. The current kit does not.
For -Force, just add it to the end of the command. It downloads the file again, overwrites it, and deletes the destination folder before extracting again.
5. vcpkg and the Libs Apache Needs
Apache httpd itself contains only its own C code. The ability to handle HTTPS, HTTP/2, and brotli compression comes from external libs. If you have installed Apache on Linux, a single apt install command gets all these libs, but on Windows nobody manages that for you. I use vcpkg, Microsoft's C/C++ package manager, to download each lib's source and compile it locally with MSVC so its ABI matches the Apache I am going to build.
manifest: Specify what you need
vcpkg has two modes. The one I use is called manifest mode: list the required libs in the project's vcpkg.json file, then run vcpkg install. This file is at deps\apache\vcpkg.json (I have shortened the baseline hash).
{
"name": "apm-apache-deps",
"version-string": "1",
"builtin-baseline": "bd8298ae21…",
"dependencies": [
"openssl", "pcre2", "zlib", "nghttp2", "brotli",
"expat", "libxml2", "jansson", "curl"
]
}
builtin-baseline is the vcpkg repository commit that pins every lib to a specific version. Without that pin, each vcpkg update could give us different lib versions without us realizing it, so builds on two machines from the same run would differ. In the actual file, the baseline is a 40-character commit id starting with bd8298ae21.
Here is what all 9 libs do, based on what httpd's configure detected in my log (the Summary of feature detection section) and what dumpbin confirms in Part 3.
| lib | Used by | Notes |
|---|---|---|
| openssl | mod_ssl, mod_md, apr_crypto_openssl |
All HTTPS/TLS (covered in Section 6) |
| pcre2 | libhttpd.dll |
Regex for mod_rewrite, <LocationMatch>, etc. |
| zlib | mod_deflate, libcurl, libxml2 |
gzip/deflate compression |
| nghttp2 | mod_http2 |
HTTP/2 |
| brotli | mod_brotli |
brotli compression |
| expat | libaprutil |
XML parsing for APR-util |
| libxml2 | mod_xml2enc |
Handles encoding in proxied XML/HTML documents |
| jansson | mod_md |
Reads/writes ACME JSON |
| curl | mod_md |
Lets mod_md communicate with Let's Encrypt |
vcpkg also pulls in what these libs need during the build, such as libiconv (required by libxml2) and vcpkg-cmake. The * ... lines in the log are entries installed as dependencies. vcpkg downloads and stores its own tools (CMake, Ninja, Perl, NASM, msys2) in toolchain\vcpkg\downloads\tools\, without touching the ones installed on your machine.
6. OpenSSL 3.5 LTS with an Overlay Port
This is a choice I made myself, and I need to explain why. The vcpkg version referenced by the baseline provides OpenSSL 3.6.5, a non-LTS release whose support ends in November 2026, which is next month. OpenSSL 3.5 is an LTS release supported until April 2030. For a web server that I intend to keep current with every security patch, I chose LTS so I would not need another major version migration soon.
vcpkg has an overlay port mechanism for exactly this: tell it to use a port (the build recipe for a lib) from our folder instead of the one in the vcpkg repository. Our folder is ports-overlay\openssl\ in the kit. It contains the complete OpenSSL recipe (portfile, patches, and vcpkg.json), set to version 3.5.9. Two places need updating whenever the version changes: the version number in vcpkg.json and the tarball's SHA512 in portfile.cmake.
{
"name": "openssl",
"version": "3.5.9",
...
}
vcpkg_from_github(
OUT_SOURCE_PATH SOURCE_PATH
REPO openssl/openssl
REF "openssl-${VERSION}"
SHA512 ab29a6ed40dd8c74…
PATCHES
cmake-config.patch
command-line-length.patch
script-prefix.patch
windows/install-layout.patch
windows/install-pdbs.patch
windows/install-programs.diff
unix/android-cc.patch
unix/move-openssldir.patch
unix/no-empty-dirs.patch
unix/no-static-libs-for-shared.patch
)
The REF "openssl-${VERSION}" line gets the number from vcpkg.json, so changing the version in one place carries through to both files. SHA512 is the hash of https://github.com/openssl/openssl/archive/openssl-3.5.9.tar.gz. If the file vcpkg downloads has a different hash, it stops just as 00-fetch.ps1 does. All 10 patches in the list come from vcpkg itself. I tested them with 3.5.9 and they worked without modification, but you must always test other versions yourself.
Building PHP in Part 6 provides another set of OpenSSL DLLs from the PHP team's deps collection (I found version 3.0.22 at that point). These two sets are separate. Apache uses the 3.5.9 I build myself, while PHP uses the PHP team's set to match the official PHP ABI. Details are in Part 6.
7. 10-deps-vcpkg.ps1 Line by Line, Then Run It
Here is the full script.
# 10-deps-vcpkg.ps1 - build Apache's third-party libs from source with vcpkg (manifest mode)
# OpenSSL comes from ports-overlay\openssl (3.5 LTS) instead of the vcpkg default.
$ErrorActionPreference = 'Stop'
$Root = Split-Path $PSScriptRoot -Parent
$Vcpkg = "$Root\toolchain\vcpkg\vcpkg.exe"
$Out = "$Root\build\vcpkg_installed"
New-Item -ItemType Directory -Force "$Root\build\logs" | Out-Null
# private vcpkg checkout (never the shared C:\vcpkg), at least as new as the manifest baseline
if (-not (Test-Path $Vcpkg)) {
$baseline = (Get-Content "$Root\deps\apache\vcpkg.json" -Raw | ConvertFrom-Json).'builtin-baseline'
git clone --quiet https://github.com/microsoft/vcpkg.git "$Root\toolchain\vcpkg"
git -C "$Root\toolchain\vcpkg" checkout --quiet $baseline
& "$Root\toolchain\vcpkg\bootstrap-vcpkg.bat" -disableMetrics
if (-not (Test-Path $Vcpkg)) { throw '[ERR] vcpkg bootstrap failed' }
}
$env:Path = ($env:Path -split ';' | Where-Object { $_ -notmatch '(?i)xampp' }) -join ';'
$env:VCPKG_DISABLE_METRICS = '1'
& $Vcpkg install --triplet x64-windows `
--x-manifest-root="$Root\deps\apache" `
--x-install-root="$Out" `
--overlay-ports="$Root\ports-overlay" `
--clean-after-build 2>&1 | Tee-Object "$Root\build\logs\vcpkg-apache.log" | Select-Object -Last 25
if ($LASTEXITCODE) { throw "[ERR] vcpkg install failed (log: build\logs\vcpkg-apache.log)" }
Write-Host "[OK] deps -> $Out\x64-windows"
Read it in execution order
- Lines 4-7 Set the paths for
vcpkg.exeand the output (build\vcpkg_installed), and createbuild\logs. - The
if (-not (Test-Path $Vcpkg))block Runs only the first time, when vcpkg is not yet present. Read the commit id frombuiltin-baselinein the manifest,git clonethe vcpkg repository intotoolchain\vcpkg,git checkoutthat commit, then runbootstrap-vcpkg.bat, which downloadsvcpkg.exe. This is deliberately a private checkout in the project folder, rather than using aC:\vcpkginstallation you may share with other work, so the two projects do not interfere with each other. - The
$env:Path = ...line Removes paths containing xampp from PATH in this process only, preventing XAMPP'sperl,php, etc. from being picked up instead of the intended tools. If you do not have XAMPP, this line does nothing. VCPKG_DISABLE_METRICSDisables sending vcpkg usage statistics to Microsoft.- The
vcpkg installcommand Has 5 options.--triplet x64-windowsSpecifies x64 with DLLs (not static), producing.dllfiles alongside.libfiles, which is what Apache needs.--x-manifest-rootPoints to the folder containingvcpkg.json.--x-install-rootInstalls the output into the project'sbuild\vcpkg_installed, rather than vcpkg's shared location.--overlay-portsPoints toports-overlayso OpenSSL 3.5.9 wins (Section 6).--clean-after-buildDeletes each lib's temporary build tree as soon as it finishes, saving several GB of disk space.
- The
2>&1 | Tee-Object ... | Select-Object -Last 25pipeline Merges errors into the output, writes the full log tobuild\logs\vcpkg-apache.log, and shows only the last 25 lines on screen. The display is short because vcpkg produces a lot of output. $LASTEXITCODEIf vcpkg returns anything other than 0, the script usesthrowand tells you where the log is.
Step 7.1 Run the script
Where: pwsh (regular user), in C:\apm-build. Expect the first run to take a while, about 45 minutes on my machine (i5-12500H), with OpenSSL and libiconv taking the longest. The screen will stay quiet for a long time during those builds; it has not hung. To check whether it is still working, open Task Manager and look for a cl.exe or nmake process using CPU.
pwsh -NoProfile -ExecutionPolicy Bypass -File scripts\10-deps-vcpkg.ps1
Step 7.2 Check the final output
The screen shows only the last 25 lines, ending with vcpkg's summary and the script's [OK]. Below is actual output from my log when I ran it again with the cache ready (which is why it says already installed). Your first run will have "will be built and installed" lines instead, but the last 3 lines will look the same apart from the time.
pcre2 provides pkg-config modules:
# Posix compatible interface to libpcre2-8
libpcre2-posix
All requested installations completed successfully in: 323 us
[OK] deps -> C:\apm-build\build\vcpkg_installed\x64-windows
The 323 us in my run means vcpkg did no work at all. On your first run, this will be measured in minutes. To see the list of installed packages, search the full log.
Select-String -Path C:\apm-build\build\logs\vcpkg-apache.log -Pattern 'x64-windows@' | ForEach-Object Line
The output on my machine (the cached run) shows every lib's version.
brotli:[email protected]
curl[core,sspi,ssl]:[email protected]
expat:[email protected]
jansson:[email protected]
* libiconv:[email protected]#1
libxml2[core,zlib,iconv]:[email protected]
nghttp2:[email protected]
openssl:[email protected]
pcre2[core,platform-default-features,jit]:[email protected]
* vcpkg-cmake:x64-windows@2025-08-07
* vcpkg-cmake-config:x64-windows@2026-07-21
* vcpkg-cmake-get-vars:x64-windows@2025-05-29
* vcpkg-make:x64-windows@2026-07-09#2
zlib:[email protected]#2
Look for openssl:[email protected] as confirmation that the overlay worked. If you see 3.6.5, the overlay was not used; go back and check that ports-overlay\openssl actually exists.
Step 7.3 Check the .lib files Apache will link against
Get-ChildItem C:\apm-build\build\vcpkg_installed\x64-windows\lib -File -Name
brotlicommon.lib
brotlidec.lib
brotlienc.lib
charset.lib
iconv.lib
jansson.lib
libcrypto.lib
libcurl.lib
libexpat.lib
libssl.lib
libxml2.lib
nghttp2.lib
pcre2-16.lib
pcre2-32.lib
pcre2-8.lib
pcre2-posix.lib
z.lib
There must be 17 files. If libssl.lib or libcrypto.lib is missing, OpenSSL did not build successfully; see "If Something Goes Wrong Here."
Step 7.4 Check the versions of the DLLs actually built
This confirms that the files copied alongside httpd.exe in Part 3 are the versions I intended, rather than files from some unknown location.
$b = 'C:\apm-build\build\vcpkg_installed\x64-windows\bin'
foreach ($n in 'libcrypto-3-x64','libssl-3-x64','libcurl','libxml2','z') { "{0,-18} {1}" -f $n, (Get-Item "$b\$n.dll").VersionInfo.ProductVersion }
libcrypto-3-x64 3.5.9
libssl-3-x64 3.5.9
libcurl 8.23.0
libxml2 2.15.4
z 1.3.2
The filename libcrypto-3-x64.dll still contains 3 even though its contents are version 3.5.9, because the number in the name is the OpenSSL ABI's major version, not the full version number.
Step 7.5 Check that vcpkg points to the correct commit
git -C C:\apm-build\toolchain\vcpkg log -1 --format=%h
bd8298ae21
It must start with the same characters as builtin-baseline in the manifest file.
Step 7.6 Confirm that the script can be run again
Run Step 7.1 again. It will finish in a few seconds because vcpkg keeps a binary cache of each package locally (by default in %LOCALAPPDATA%\vcpkg\archives). This means that changing one lib's version does not recompile the others. When I measured a repeat run of the entire build.cmd, the vcpkg step took 0.1 minutes.
You have C:\apm-build\build\vcpkg_installed\x64-windows, containing include\, lib\ (17 .lib files), and bin\ (18 DLLs). This is everything Part 3 will pull in.
8. If Something Goes Wrong Here
This table covers issues I actually encountered or that the scripts are designed to catch. Where I have not encountered an issue myself, I have marked it as based on reading the code.
| Symptom | Cause | Fix |
|---|---|---|
[ERR] sha256 mismatch <ชื่อ> with got and want |
The downloaded file does not match the pinned hash. The download may be incomplete, the mirror may have served the wrong file, or the upstream file may have changed. | Delete that file from downloads\ and run again. If the same thing happens, do not just change the hash to make it pass. Compare it against the upstream page first (Step 4.8). |
download failed: <url> |
Connection lost, corporate proxy blocking the download, or upstream URL changed. | Try opening the URL in your browser. If that works but curl.exe does not, check the proxy. Leftover .part files can be deleted. |
The run is silent, with no [OK] lines |
A name in -Only does not match a key in versions.json (for example apache instead of httpd), so every entry is skipped. Or an older kit that does not split a,b itself. |
Compare the name with the keys in versions.json. With an old kit, download the new one, or run through -Command "& .\scripts�-fetch.ps1 -Only a,b". |
[OK] <ชื่อ> (already extracted) appears, but files inside are missing |
Extraction stopped midway (disk full). The destination folder had already been created, so the next run sees it and skips extraction (based on reading the code). | Delete that folder from src\ or toolchain\ and run again, or add -Force. |
extract failed: <ไฟล์> |
tar.exe cannot read the file, the file is corrupt, or the path is too long. |
Delete the file from downloads\ and download it again. If it still happens, check that the project folder is not nested too deeply. Use C:\apm-build as instructed. |
A parameter cannot be found that matches parameter name 'AsHashtable' |
Running in Windows PowerShell 5.1 instead of pwsh 7. | Open pwsh (Step 1.1). |
OpenSSL .lib files are missing / [ERR] vcpkg install failed |
One of the libs failed to build. | Open build\logs\vcpkg-apache.log and look for the first error message. vcpkg usually gives the package's individual log filename near the end. |
| OpenSSL fails with strange Unix-style paths | Git's perl (cygwin) was picked up from PATH. | Follow Step 4.6 until you get MSWin32, and remove Git's perl folder from that session's PATH if it is set there. |
warning: ... ignoring mismatched VCPKG_ROOT environment value C:\vcpkg |
A VCPKG_ROOT variable is set for another vcpkg installation on the machine. |
This is only a warning, not a problem. vcpkg uses the project's copy as intended (seen in my actual log). |
| It takes twice as long | Defender is scanning tens of thousands of .obj files. |
Set the exclusion described in Part 1 (requires admin). |
| Running again still recompiles OpenSSL even though nothing changed | The binary cache in %LOCALAPPDATA%\vcpkg\archives was deleted, or the compiler changed (updating MSVC changes the compiler hash). Based on vcpkg's behavior of including the compiler in its hash. |
Let it finish. The next run will be fast again. |
If you encounter a symptom outside the table, always open the log first. Console output is limited to the last 25 lines, but the full log is at C:\apm-build\build\logs\vcpkg-apache.log, and the first error message identifying the actual cause is often above those 25 lines.
9. Checklist Before Part 3
Every item must be true before continuing. If any item fails, return to the referenced step.
C:\apm-build\downloadscontains 13 files, and running00-fetch.ps1again reportsalready extractedon every entry (Steps 4.3 and 4.7).C:\apm-build\srccontains 9 folders, andTest-PathreturnsTruefor all fourCMakeLists.txtfiles (Steps 4.4 and 4.5).- Strawberry's
perl.exereportsMSWin32(Step 4.6). C:\apm-build\build\vcpkg_installed\x64-windows\libcontains 17.libfiles, includinglibssl.libandlibcrypto.lib(Step 7.3).libcrypto-3-x64.dllhas a ProductVersion of3.5.9(Step 7.4).C:\apm-build\build\logs\vcpkg-apache.logends withAll requested installations completed successfully(Step 7.2).
If you just want to use APM without compiling anything, the finished build is at github.com/ChonLaWan/APM-Server. Download APM-1.0.0-win64.zip (about 140 MB) from Releases, compare its sha256 with the attached .sha256 file, unzip it anywhere and run manager\APMManager.exe. The Manager picks free ports, writes the config, issues the certificate and creates the database for you. Its Check for updates button downloads a new release, verifies the sha256 and swaps the binaries without touching your config, databases or sites.
Contents · Previous: Part 1: Prepare the Tools · Next: Part 3: Build Apache httpd
Part 3 will bring together the source in src\ and the libs in build\vcpkg_installed to build Apache httpd and mod_fcgid, and you will see how the script finds the DLLs that need to sit alongside httpd.exe. If you have followed along this far, the longest part of building Apache is behind you. I will come back and make further edits later.
Want us to set this up?
Get help with server setup, configuration, migration, or troubleshooting. Save time for the work that matters.
Hit something like this? Stuck somewhere?
Come trade notes on the LinuxThai board



Comments 0
No comments yet — be the first