#!/usr/bin/env bash set -euo pipefail usage() { cat <<'EOF' Usage: script/build_msquic.sh [options] Build the pinned MsQuic source into: dependency//third_party/msquic/v 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" </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}" echo "Configure cmvr-es with:" echo " -DCMVR_ENABLE_MSQUIC_BACKEND=ON -DCMVR_REQUIRE_MSQUIC=ON"