blob: ce721d989bf3ced8c3935c2319a75b4e8bb87c22 [file]
#!/bin/bash
# Copyright (c) 2026 Project CHIP Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Builds an MSan-instrumented sysroot for Matter MSAN tests.
#
# The sysroot contains libc++/libc++abi and a few common third-party
# dependencies (OpenSSL, zlib, libffi, pcre2, GLib) all compiled with
# -fsanitize=memory so MSAN reports are not poisoned by uninstrumented
# library internals.
#
# Idempotent: if the sysroot is already built and its inputs (this script,
# msan_ignorelist.txt, clang version) have not changed, the script exits
# in milliseconds. First-time build takes ~5-15 minutes.
#
# Prerequisites: source scripts/activate.sh
#
# Usage:
# scripts/build/build_msan_sysroot.sh [--out-dir PATH] [--force] [--help]
#
# Environment:
# SYSROOT_MSAN Override install location (default: ~/.cache/matter/msan_sysroot)
set -euo pipefail
CHIP_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
PW_CLANG="$(command -v clang || true)"
PW_CLANGXX="$(command -v clang++ || true)"
DEFAULT_SYSROOT="$HOME/.cache/matter/msan_sysroot"
IGNORELIST="$CHIP_ROOT/build/config/compiler/msan_ignorelist.txt"
SCRIPT_PATH="$(cd "$(dirname "$0")" && pwd)/$(basename "$0")"
SRC="${TMPDIR:-/tmp}/msan-build"
MSAN="-fsanitize=memory -fsanitize-memory-track-origins -fno-omit-frame-pointer -fPIC"
# Pinned for reproducibility and to keep msan_ignorelist.txt source paths
# matching. On bump: verify clean MSAN build, no new false positives, and
# ignorelist paths still match. libc++/libc++abi track Pigweed clang.
OPENSSL_VERSION="3.5.3"
ZLIB_VERSION="1.3.1"
LIBFFI_VERSION="3.4.6"
PCRE2_VERSION="10.43"
GLIB_VERSION="2.80.0"
GLIB_SERIES="${GLIB_VERSION%.*}" # GNOME download path uses major.minor (e.g. 2.80)
usage() {
cat <<EOF
Usage: $(basename "$0") [--out-dir PATH] [--force] [--check] [--help]
Builds an MSan-instrumented sysroot for Matter MSAN tests.
Options:
--out-dir PATH Install sysroot to PATH (overrides SYSROOT_MSAN)
--force Rebuild even if the existing sysroot is current
--check Report freshness only; exit 0 if current, 2 if missing,
3 if stale. Does not start a build.
--help Show this message
Environment:
SYSROOT_MSAN Default install location override
(default: ~/.cache/matter/msan_sysroot)
The script is idempotent: if the sysroot is already built and its inputs
(this script, msan_ignorelist.txt, clang version) have not changed, it
exits in milliseconds. A first-time build takes 5-15 minutes.
build_examples.py reads the resolved sysroot path via host.py and passes
it to GN as msan_sysroot="<path>". Raw \`gn gen\` users must export
SYSROOT_MSAN or pass --args='msan_sysroot="..."' explicitly.
EOF
}
FORCE=0
CHECK_ONLY=0
OUT_DIR=""
while [[ $# -gt 0 ]]; do
case "$1" in
--out-dir)
OUT_DIR="$2"
shift 2
;;
--force)
FORCE=1
shift
;;
--check)
CHECK_ONLY=1
shift
;;
--help | -h)
usage
exit 0
;;
*)
echo "Unknown argument: $1" >&2
usage >&2
exit 1
;;
esac
done
SYSROOT="${OUT_DIR:-${SYSROOT_MSAN:-$DEFAULT_SYSROOT}}"
STAMP="$SYSROOT/.build_complete"
LOCKFILE="$SYSROOT/.build.lock"
if [[ ! -x "$PW_CLANG" ]] || [[ ! -x "$PW_CLANGXX" ]]; then
echo "ERROR: Pigweed clang/clang++ not found on PATH" >&2
echo " clang: ${PW_CLANG:-<none>}" >&2
echo " clang++: ${PW_CLANGXX:-<none>}" >&2
echo "Run: source scripts/activate.sh" >&2
exit 1
fi
LLVM_COMMIT="$("$PW_CLANG" --version | grep -oP '[0-9a-f]{40}' || true)"
if [[ -z "$LLVM_COMMIT" ]]; then
echo "ERROR: clang on PATH is not Pigweed's (no LLVM commit in --version)" >&2
echo " clang: $PW_CLANG" >&2
echo "Run: source scripts/activate.sh" >&2
exit 1
fi
if [[ ! -f "$IGNORELIST" ]]; then
echo "ERROR: $IGNORELIST not found" >&2
exit 1
fi
# Hash inputs that determine sysroot contents. Path-independent so local
# and CI agree even when CHIP_ROOT differs. Format: each file contributes
# only its content sha (no path), followed by clang --version output.
# This hash is used by this script's --check mode to detect staleness for
# callers.
compute_input_hash() {
{
sha256sum <"$SCRIPT_PATH" | cut -d' ' -f1
sha256sum <"$IGNORELIST" | cut -d' ' -f1
"$PW_CLANG" --version
} | sha256sum | cut -d' ' -f1
}
EXPECTED_HASH="$(compute_input_hash)"
# --check mode: report sysroot freshness for callers (e.g. host.py) and exit.
# Exit 0 on fresh, 2 on missing stamp, 3 on stale stamp. No build is started.
if [[ "$CHECK_ONLY" -eq 1 ]]; then
if [[ ! -f "$STAMP" ]]; then
echo "MSAN sysroot missing at $SYSROOT (no stamp file)" >&2
exit 2
fi
if [[ "$(cat "$STAMP")" != "$EXPECTED_HASH" ]]; then
echo "MSAN sysroot stale at $SYSROOT (inputs changed since build)" >&2
exit 3
fi
echo "MSAN sysroot OK at $SYSROOT"
exit 0
fi
# Stamp check: short-circuit if up-to-date.
if [[ "$FORCE" -eq 0 ]] && [[ -f "$STAMP" ]] && [[ "$(cat "$STAMP")" == "$EXPECTED_HASH" ]]; then
echo "MSAN sysroot up-to-date at $SYSROOT"
exit 0
fi
mkdir -p "$SYSROOT" "$SRC"
# Serialize concurrent invocations on the same sysroot dir.
# flock blocks until the lock is acquired; a second concurrent build
# will see the stamp file written by the first and short-circuit on retry.
exec 9>"$LOCKFILE"
if ! flock -x -n 9; then
echo "Another build is in progress on $SYSROOT, waiting..."
flock -x 9
fi
# After acquiring the lock, re-check the stamp: another process may
# have completed the build while we were blocked.
if [[ "$FORCE" -eq 0 ]] && [[ -f "$STAMP" ]] && [[ "$(cat "$STAMP")" == "$EXPECTED_HASH" ]]; then
echo "MSAN sysroot built by concurrent process; reusing"
exit 0
fi
# Clean up stamp file on failure so the next invocation rebuilds
# from scratch instead of trusting half-finished state.
on_error() {
echo "ERROR: build failed; removing stamp at $STAMP" >&2
rm -f "$STAMP"
}
trap on_error ERR
echo ">>> Building MSAN sysroot at $SYSROOT (this takes 5-15 min)"
# libc++ / libc++abi (MSan-instrumented)
echo ">>> libc++ / libc++abi"
if [[ ! -d "$SRC/llvm-project" ]]; then
git clone --depth 1 https://llvm.googlesource.com/llvm-project "$SRC/llvm-project"
fi
cd "$SRC/llvm-project"
git fetch --depth 1 origin "$LLVM_COMMIT"
git checkout FETCH_HEAD
cmake -GNinja -S "$SRC/llvm-project/runtimes" -B "$SRC/libcxx" \
-DCMAKE_C_COMPILER="$PW_CLANG" -DCMAKE_CXX_COMPILER="$PW_CLANGXX" \
-DLLVM_ENABLE_RUNTIMES="libcxx;libcxxabi" -DCMAKE_BUILD_TYPE=Release \
-DLLVM_USE_SANITIZER=MemoryWithOrigins -DCMAKE_INSTALL_PREFIX="$SYSROOT" \
-DLIBCXX_ENABLE_SHARED=OFF -DLIBCXXABI_ENABLE_SHARED=OFF -DLIBCXXABI_USE_LLVM_UNWINDER=OFF
ninja -C "$SRC/libcxx" -j"$(nproc)" cxx cxxabi
ninja -C "$SRC/libcxx" install-cxx install-cxxabi
# OpenSSL
echo ">>> OpenSSL"
cd "$SRC"
[[ -d "openssl-$OPENSSL_VERSION" ]] || { wget -q "https://www.openssl.org/source/openssl-$OPENSSL_VERSION.tar.gz" && tar xzf "openssl-$OPENSSL_VERSION.tar.gz"; }
cd "openssl-$OPENSSL_VERSION"
CC="$PW_CLANG $MSAN" ./Configure linux-x86_64 --prefix="$SYSROOT" --openssldir="$SYSROOT/ssl" no-asm no-shared -DPURIFY
make -j"$(nproc)" build_libs
make install_dev
# zlib
echo ">>> zlib"
cd "$SRC"
[[ -d "zlib-$ZLIB_VERSION" ]] || { wget -q -O - "https://github.com/madler/zlib/releases/download/v$ZLIB_VERSION/zlib-$ZLIB_VERSION.tar.gz" | tar xz; }
cd "zlib-$ZLIB_VERSION"
CC="$PW_CLANG" CFLAGS="$MSAN" LDFLAGS="-fsanitize=memory" ./configure --prefix="$SYSROOT" --static
make -j"$(nproc)" && make install
# libffi
echo ">>> libffi"
cd "$SRC"
[[ -d "libffi-$LIBFFI_VERSION" ]] || { wget -q "https://github.com/libffi/libffi/releases/download/v$LIBFFI_VERSION/libffi-$LIBFFI_VERSION.tar.gz" && tar xzf "libffi-$LIBFFI_VERSION.tar.gz"; }
cd "libffi-$LIBFFI_VERSION"
CC="$PW_CLANG" CXX="$PW_CLANGXX" CFLAGS="$MSAN" CXXFLAGS="$MSAN" LDFLAGS="-fsanitize=memory" \
./configure --prefix="$SYSROOT" --disable-shared --enable-static --quiet
make -j"$(nproc)" && make install
# pcre2
echo ">>> pcre2"
cd "$SRC"
[[ -d "pcre2-$PCRE2_VERSION" ]] || { wget -q "https://github.com/PCRE2Project/pcre2/releases/download/pcre2-$PCRE2_VERSION/pcre2-$PCRE2_VERSION.tar.gz" && tar xzf "pcre2-$PCRE2_VERSION.tar.gz"; }
cd "pcre2-$PCRE2_VERSION"
CC="$PW_CLANG" CXX="$PW_CLANGXX" CFLAGS="$MSAN" CXXFLAGS="$MSAN" LDFLAGS="-fsanitize=memory" \
./configure --prefix="$SYSROOT" --disable-shared --enable-static --quiet
make -j"$(nproc)" && make install
# GLib
echo ">>> GLib"
cd "$SRC"
[[ -d "glib-$GLIB_VERSION" ]] || { wget -q "https://download.gnome.org/sources/glib/$GLIB_SERIES/glib-$GLIB_VERSION.tar.xz" && tar xf "glib-$GLIB_VERSION.tar.xz"; }
cat >"$SRC/msan-native.ini" <<EOF
[binaries]
c = '$PW_CLANG'
cpp = '$PW_CLANGXX'
ar = 'ar'
strip = 'strip'
pkgconfig = 'pkg-config'
[built-in options]
c_args = ['-fsanitize=memory', '-fsanitize-memory-track-origins', '-fno-omit-frame-pointer', '-fPIC', '-Wno-error=implicit-function-declaration', '-fsanitize-ignorelist=$IGNORELIST']
cpp_args = ['-fsanitize=memory', '-fsanitize-memory-track-origins', '-fno-omit-frame-pointer', '-fPIC', '-fsanitize-ignorelist=$IGNORELIST']
c_link_args = ['-fsanitize=memory']
cpp_link_args = ['-fsanitize=memory']
[properties]
pkg_config_path = '$SYSROOT/lib/pkgconfig:$SYSROOT/lib64/pkgconfig'
EOF
cd "glib-$GLIB_VERSION"
rm -rf builddir
PKG_CONFIG_PATH="$SYSROOT/lib/pkgconfig:$SYSROOT/lib64/pkgconfig" \
meson setup builddir --native-file "$SRC/msan-native.ini" --prefix="$SYSROOT" \
--libdir=lib \
--default-library=static --buildtype=debugoptimized \
-Dselinux=disabled -Dxattr=false -Dlibmount=disabled -Dnls=disabled \
-Dtests=false -Dglib_checks=false -Dglib_assert=false \
-Dintrospection=disabled -Ddtrace=false -Dsystemtap=false \
-Dman-pages=disabled -Ddocumentation=false -Dbsymbolic_functions=false
ninja -C builddir -j"$(nproc)" && ninja -C builddir install
# Stamp file written last; only exists on full success.
echo "$EXPECTED_HASH" >"$STAMP"
trap - ERR
echo ">>> Sysroot ready at $SYSROOT"