cmvr-es/script/build_msquic.sh

412 lines
14 KiB
Bash
Raw Normal View History

#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage: script/build_msquic.sh [options]
Build the pinned MsQuic source into:
dependency/<arch>/third_party/msquic/v<version>
Options:
--arch x86|arm Dependency architecture (default: native host)
--version VERSION Supported pinned version without leading v (default: 2.5.9)
--jobs N Parallel build jobs (default: nproc)
--clean Recreate the MsQuic build and staging directories
-h, --help Show this help
Environment:
CMVR_CMAKE Absolute CMake executable override
CMVR_MSQUIC_TOOLCHAIN_FILE CMake toolchain file for cross-compilation
CC, CXX Native compiler overrides
The first run needs network access to clone the official MsQuic tag and its
QuicTLS submodule. No sudo or system MsQuic installation is used.
EOF
}
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
repo_root="$(cd -- "${script_dir}/.." && pwd -P)"
version="2.5.9"
arch=""
jobs=""
clean_build=false
while (($# > 0)); do
case "$1" in
--arch)
[[ $# -ge 2 ]] || { echo "missing value for --arch" >&2; exit 2; }
arch="$2"
shift 2
;;
--version)
[[ $# -ge 2 ]] || { echo "missing value for --version" >&2; exit 2; }
version="${2#v}"
shift 2
;;
--jobs)
[[ $# -ge 2 ]] || { echo "missing value for --jobs" >&2; exit 2; }
jobs="$2"
shift 2
;;
--clean)
clean_build=true
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "unknown option: $1" >&2
usage >&2
exit 2
;;
esac
done
host_machine="$(uname -m)"
case "${host_machine}" in
x86_64|amd64)
native_arch="x86"
;;
aarch64|arm64|armv8*)
native_arch="arm"
;;
*)
echo "unsupported host architecture: ${host_machine}" >&2
exit 2
;;
esac
arch="${arch:-${native_arch}}"
if [[ "${arch}" != "x86" && "${arch}" != "arm" ]]; then
echo "--arch must be x86 or arm" >&2
exit 2
fi
if [[ ! "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "--version must use the form MAJOR.MINOR.PATCH" >&2
exit 2
fi
case "${version}" in
2.5.9)
expected_source_commit="87b53085d76bd7920d490a6f226c9999b6614d14"
;;
*)
echo "unsupported MsQuic version: ${version}" >&2
echo "add its reviewed tag and commit to script/build_msquic.sh first" >&2
exit 2
;;
esac
if [[ -z "${jobs}" ]]; then
jobs="$(nproc 2>/dev/null || getconf _NPROCESSORS_ONLN || echo 2)"
fi
if [[ ! "${jobs}" =~ ^[1-9][0-9]*$ ]]; then
echo "--jobs must be a positive integer" >&2
exit 2
fi
toolchain_args=()
if [[ "${arch}" != "${native_arch}" ]]; then
if [[ -z "${CMVR_MSQUIC_TOOLCHAIN_FILE:-}" ]]; then
echo "cross-building ${arch} on ${host_machine} requires" >&2
echo "CMVR_MSQUIC_TOOLCHAIN_FILE=/absolute/path/to/toolchain.cmake" >&2
exit 2
fi
if [[ ! -f "${CMVR_MSQUIC_TOOLCHAIN_FILE}" ]]; then
echo "toolchain file does not exist: ${CMVR_MSQUIC_TOOLCHAIN_FILE}" >&2
exit 2
fi
toolchain_file="$(realpath "${CMVR_MSQUIC_TOOLCHAIN_FILE}")"
toolchain_args+=("-DCMAKE_TOOLCHAIN_FILE=${toolchain_file}")
fi
tag="v${version}"
source_dir="${repo_root}/build/third_party/msquic-src/${tag}"
build_dir="${repo_root}/build/third_party/msquic-build/${arch}-${tag}"
stage_dir="${repo_root}/build/third_party/msquic-stage/${arch}-${tag}"
stage_prefix="${stage_dir}/prefix"
install_root="${repo_root}/dependency/${arch}/third_party/msquic/${tag}"
backup_root="${install_root}.previous"
for guarded_path in \
"${source_dir}" "${build_dir}" "${stage_dir}" \
"${install_root}" "${backup_root}"; do
case "${guarded_path}" in
"${repo_root}"/build/third_party/*|\
"${repo_root}"/dependency/"${arch}"/third_party/msquic/"${tag}"|\
"${repo_root}"/dependency/"${arch}"/third_party/msquic/"${tag}".previous)
;;
*)
echo "refusing unsafe path: ${guarded_path}" >&2
exit 2
;;
esac
done
if [[ -n "${CMVR_CMAKE:-}" ]]; then
cmake_bin="$(realpath "${CMVR_CMAKE}")"
else
bundled_cmake="${repo_root}/dependency/${arch}/third_party/cmake/v3.30.3/cmake-3.30.3-linux-$(
[[ "${arch}" == "x86" ]] && echo x86_64 || echo aarch64
)/bin/cmake"
if [[ "${arch}" == "${native_arch}" && -x "${bundled_cmake}" ]]; then
cmake_bin="${bundled_cmake}"
else
cmake_bin="$(command -v cmake)"
fi
fi
[[ -x "${cmake_bin}" ]] || {
echo "CMake executable is unavailable: ${cmake_bin}" >&2
exit 2
}
git_bin="/usr/bin/git"
[[ -x "${git_bin}" ]] || git_bin="$(command -v git)"
[[ -x "${git_bin}" ]] || { echo "git is required" >&2; exit 2; }
clean_path="$(dirname "${cmake_bin}"):/usr/local/bin:/usr/bin:/bin"
export PATH="${clean_path}"
unset CONDA_PREFIX CONDA_DEFAULT_ENV CMAKE_PREFIX_PATH PKG_CONFIG_PATH \
OPENSSL_ROOT_DIR OPENSSL_DIR LD_LIBRARY_PATH LIBRARY_PATH CPATH \
C_INCLUDE_PATH CPLUS_INCLUDE_PATH CFLAGS CXXFLAGS CPPFLAGS LDFLAGS \
PERL5LIB PERL5OPT
if [[ "${arch}" == "${native_arch}" ]]; then
export CC="${CC:-/usr/bin/cc}"
export CXX="${CXX:-/usr/bin/c++}"
fi
mkdir -p "$(dirname "${source_dir}")" "$(dirname "${install_root}")"
if [[ ! -d "${source_dir}/.git" ]]; then
"${git_bin}" clone \
--branch "${tag}" \
--depth 1 \
https://github.com/microsoft/msquic.git \
"${source_dir}"
fi
source_commit="$("${git_bin}" -C "${source_dir}" rev-parse HEAD)"
tag_commit="$("${git_bin}" -C "${source_dir}" rev-list -n 1 "${tag}")"
if [[ "${source_commit}" != "${tag_commit}" ]]; then
echo "${source_dir} is not checked out at ${tag}" >&2
echo "remove that cache directory and rerun the script" >&2
exit 2
fi
if [[ "${source_commit}" != "${expected_source_commit}" ]]; then
echo "${tag} resolved to an unexpected source commit" >&2
echo "expected: ${expected_source_commit}" >&2
echo "actual: ${source_commit}" >&2
exit 2
fi
unexpected_initialized_submodules="$(
"${git_bin}" -C "${source_dir}" submodule status |
awk '$2 != "submodules/quictls" && substr($1, 1, 1) != "-" {
print $2 " (" $1 ")"
}'
)"
if [[ -n "${unexpected_initialized_submodules}" ]]; then
echo "MsQuic source cache contains initialized non-QuicTLS submodules:" >&2
echo "${unexpected_initialized_submodules}" >&2
echo "deinitialize those submodules or use a clean source cache before building" >&2
exit 2
fi
"${git_bin}" -C "${source_dir}" submodule sync -- submodules/quictls
"${git_bin}" -C "${source_dir}" submodule update \
--init --depth 1 -- submodules/quictls
source_changes="$("${git_bin}" -C "${source_dir}" status \
--porcelain --untracked-files=all --ignore-submodules=all)"
if [[ -n "${source_changes}" ]]; then
echo "MsQuic source cache contains local changes:" >&2
echo "${source_changes}" >&2
echo "use a clean source cache before building" >&2
exit 2
fi
quictls_dir="${source_dir}/submodules/quictls"
expected_quictls_commit="$("${git_bin}" -C "${source_dir}" \
rev-parse HEAD:submodules/quictls)"
actual_quictls_commit="$("${git_bin}" -C "${quictls_dir}" rev-parse HEAD)"
quictls_changes="$("${git_bin}" -C "${quictls_dir}" status \
--porcelain --untracked-files=all)"
if [[ "${actual_quictls_commit}" != "${expected_quictls_commit}" ||
-n "${quictls_changes}" ]]; then
echo "QuicTLS source cache is not at the clean pinned commit" >&2
echo "expected: ${expected_quictls_commit}" >&2
echo "actual: ${actual_quictls_commit}" >&2
[[ -z "${quictls_changes}" ]] || echo "${quictls_changes}" >&2
exit 2
fi
if [[ "${clean_build}" == true ]]; then
"${cmake_bin}" -E remove_directory "${build_dir}"
fi
"${cmake_bin}" -E remove_directory "${stage_dir}"
"${cmake_bin}" -E make_directory "${build_dir}" "${stage_prefix}"
toolchain_fingerprint="native"
if [[ ${#toolchain_args[@]} -ne 0 ]]; then
toolchain_fingerprint="$(
sha256sum "${toolchain_file}" | awk '{print $1}'
)"
fi
build_recipe_version="5"
build_fingerprint="$(
printf '%s' \
"${source_commit}|${build_recipe_version}|${arch}|" \
"${CC:-toolchain}|${CXX:-toolchain}|" \
"${toolchain_fingerprint}|${cmake_bin}"
)"
fingerprint_file="${build_dir}/cmvr-msquic-build.fingerprint"
if [[ -f "${build_dir}/CMakeCache.txt" ]]; then
if [[ ! -f "${fingerprint_file}" ]]; then
echo "existing MsQuic build cache predates compiler fingerprinting" >&2
echo "rerun with --clean" >&2
exit 2
fi
existing_fingerprint="$(<"${fingerprint_file}")"
if [[ "${existing_fingerprint}" != "${build_fingerprint}" ]]; then
echo "MsQuic compiler/toolchain fingerprint changed" >&2
echo "rerun with --clean" >&2
exit 2
fi
fi
printf '%s\n' "${build_fingerprint}" >"${fingerprint_file}"
prefix_map_flags="\
-ffile-prefix-map=${repo_root}=. -fmacro-prefix-map=${repo_root}=."
"${cmake_bin}" \
-S "${source_dir}" \
-B "${build_dir}" \
-G "Unix Makefiles" \
-DCMAKE_BUILD_TYPE=Release \
"-DCMAKE_INSTALL_PREFIX=${stage_prefix}" \
"-DCMAKE_MODULE_PATH=${repo_root}/cmake/msquic" \
"-DCMVR_MSQUIC_PROCESSOR_COUNT=${jobs}" \
"-DCMAKE_C_FLAGS=${prefix_map_flags}" \
"-DCMAKE_CXX_FLAGS=${prefix_map_flags}" \
-DQUIC_BUILD_SHARED=ON \
-DQUIC_BUILD_TEST=OFF \
-DQUIC_BUILD_TOOLS=OFF \
-DQUIC_BUILD_PERF=OFF \
-DQUIC_ENABLE_LOGGING=OFF \
-DQUIC_TLS_LIB=quictls \
-DQUIC_USE_SYSTEM_LIBCRYPTO=OFF \
-DNUMA:STRING=FALSE \
"${toolchain_args[@]}"
# QuicTLS derives MODULESDIR from its temporary --prefix and compiles that
# absolute path into libcrypto. Dynamic providers are disabled above
# (no-shared, no-legacy and no-fips), so keep the unused fallback path stable
# instead of leaking the build workspace into the shipped MsQuic runtime.
openssl_makefile_target="_deps/opensslquic-build/submodules/quictls/Makefile"
openssl_build_rules="_deps/opensslquic-build/CMakeFiles/OpenSSL_Target.dir/build.make"
make_bin="$(command -v make)"
[[ -x "${make_bin}" ]] || {
echo "GNU Make is required to configure the bundled QuicTLS source" >&2
exit 2
}
"${make_bin}" -C "${build_dir}" -f "${openssl_build_rules}" \
"${openssl_makefile_target}"
openssl_makefile="${build_dir}/${openssl_makefile_target}"
test -f "${openssl_makefile}"
if ! grep -Fx 'MODULESDIR=/usr/lib/ssl/ossl-modules' \
"${openssl_makefile}" >/dev/null; then
sed -i -E \
's|^MODULESDIR=.*$|MODULESDIR=/usr/lib/ssl/ossl-modules|' \
"${openssl_makefile}"
fi
grep -Fx 'MODULESDIR=/usr/lib/ssl/ossl-modules' \
"${openssl_makefile}" >/dev/null
"${cmake_bin}" --build "${build_dir}" --parallel "${jobs}"
"${cmake_bin}" --install "${build_dir}"
# Keep only the public Linux API and shared runtime that cmvr-es consumes.
find "${stage_prefix}/include" -maxdepth 1 -type f \
! -name msquic.h \
! -name msquic_posix.h \
! -name quic_sal_stub.h \
-delete
"${cmake_bin}" -E rm -f "${stage_prefix}/lib/libmsquic_platform.a"
"${cmake_bin}" -E remove_directory "${stage_prefix}/share"
"${cmake_bin}" -E make_directory "${stage_prefix}/share/licenses/msquic"
"${cmake_bin}" -E copy "${source_dir}/LICENSE" \
"${stage_prefix}/share/licenses/msquic/LICENSE"
"${cmake_bin}" -E copy "${source_dir}/THIRD-PARTY-NOTICES" \
"${stage_prefix}/share/licenses/msquic/THIRD-PARTY-NOTICES"
cat >"${stage_prefix}/BUILD-INFO.txt" <<EOF
msquic_version=${version}
source_tag=${tag}
source_commit=${source_commit}
dependency_arch=${arch}
tls_backend=quictls-static
dynamic_tls_providers=false
system_libcrypto=false
numa=false
EOF
test -f "${stage_prefix}/include/msquic.h"
test -f "${stage_prefix}/include/msquic_posix.h"
test -f "${stage_prefix}/include/quic_sal_stub.h"
test -f "${stage_prefix}/lib/libmsquic.so.${version}"
test -L "${stage_prefix}/lib/libmsquic.so"
if command -v readelf >/dev/null 2>&1; then
if readelf -d "${stage_prefix}/lib/libmsquic.so.${version}" |
grep -E 'NEEDED.*lib(ssl|crypto|numa)' >/dev/null; then
echo "MsQuic unexpectedly depends on system TLS or NUMA libraries" >&2
exit 1
fi
fi
if grep -R -F --exclude='libmsquic.so*' \
"${repo_root}" "${stage_prefix}" >/dev/null 2>&1; then
echo "staged MsQuic metadata contains a non-relocatable workspace path" >&2
exit 1
fi
if command -v strings >/dev/null 2>&1 &&
strings "${stage_prefix}/lib/libmsquic.so.${version}" |
grep -F "${repo_root}" >/dev/null; then
echo "staged MsQuic runtime contains a non-relocatable workspace path" >&2
exit 1
fi
swap_in_progress=false
restore_install_on_exit() {
if [[ "${swap_in_progress}" != true ]]; then
return
fi
if [[ -e "${install_root}" ]]; then
"${cmake_bin}" -E remove_directory "${backup_root}" || true
elif [[ -e "${backup_root}" ]]; then
mv "${backup_root}" "${install_root}" || true
fi
}
trap restore_install_on_exit EXIT
if [[ -e "${backup_root}" ]]; then
if [[ ! -e "${install_root}" ]]; then
mv "${backup_root}" "${install_root}"
else
echo "stale MsQuic backup requires manual inspection:" >&2
echo " ${backup_root}" >&2
exit 2
fi
fi
if [[ -e "${install_root}" ]]; then
swap_in_progress=true
mv "${install_root}" "${backup_root}"
fi
mv "${stage_prefix}" "${install_root}"
swap_in_progress=false
"${cmake_bin}" -E remove_directory "${backup_root}"
trap - EXIT
echo "MsQuic ${tag} installed to:"
echo " ${install_root}"
2026-07-27 17:47:30 +08:00
echo "MsQuic is enabled through its request.txt dependency entry."