#!/usr/bin/env bash # Copyright (c) the JPEG XL Project Authors. All rights reserved. # # Use of this source code is governed by a BSD-style # license that can be found in the LICENSE file. # Continuous integration helper module. This module is meant to be called from # the .gitlab-ci.yml file during the continuous integration build, as well as # from the command line for developers. set -eu OS=`uname -s` MYDIR=$(dirname $(realpath "$0")) ### Environment parameters: TEST_STACK_LIMIT="${TEST_STACK_LIMIT:-128}" CMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE:-RelWithDebInfo} CMAKE_PREFIX_PATH=${CMAKE_PREFIX_PATH:-} CMAKE_C_COMPILER_LAUNCHER=${CMAKE_C_COMPILER_LAUNCHER:-} CMAKE_CXX_COMPILER_LAUNCHER=${CMAKE_CXX_COMPILER_LAUNCHER:-} CMAKE_MAKE_PROGRAM=${CMAKE_MAKE_PROGRAM:-} SKIP_TEST="${SKIP_TEST:-0}" TEST_SELECTOR="${TEST_SELECTOR:-}" BUILD_TARGET="${BUILD_TARGET:-}" ENABLE_WASM_SIMD="${ENABLE_WASM_SIMD:-0}" if [[ -n "${BUILD_TARGET}" ]]; then BUILD_DIR="${BUILD_DIR:-${MYDIR}/build-${BUILD_TARGET%%-*}}" else BUILD_DIR="${BUILD_DIR:-${MYDIR}/build}" fi # Whether we should post a message in the MR when the build fails. POST_MESSAGE_ON_ERROR="${POST_MESSAGE_ON_ERROR:-1}" # Set default compilers to clang if not already set export CC=${CC:-clang} export CXX=${CXX:-clang++} # Time limit for the "fuzz" command in seconds (0 means no limit). FUZZER_MAX_TIME="${FUZZER_MAX_TIME:-0}" SANITIZER="none" if [[ "${BUILD_TARGET%%-*}" == "x86_64" || "${BUILD_TARGET%%-*}" == "i686" ]]; then # Default to building all targets, even if compiler baseline is SSE4 HWY_BASELINE_TARGETS=${HWY_BASELINE_TARGETS:-HWY_SCALAR} else HWY_BASELINE_TARGETS=${HWY_BASELINE_TARGETS:-} fi # Convenience flag to pass both CMAKE_C_FLAGS and CMAKE_CXX_FLAGS CMAKE_FLAGS=${CMAKE_FLAGS:-} CMAKE_C_FLAGS="${CMAKE_C_FLAGS:-} ${CMAKE_FLAGS}" CMAKE_CXX_FLAGS="${CMAKE_CXX_FLAGS:-} ${CMAKE_FLAGS}" CMAKE_CROSSCOMPILING_EMULATOR=${CMAKE_CROSSCOMPILING_EMULATOR:-} CMAKE_EXE_LINKER_FLAGS=${CMAKE_EXE_LINKER_FLAGS:-} CMAKE_FIND_ROOT_PATH=${CMAKE_FIND_ROOT_PATH:-} CMAKE_MODULE_LINKER_FLAGS=${CMAKE_MODULE_LINKER_FLAGS:-} CMAKE_SHARED_LINKER_FLAGS=${CMAKE_SHARED_LINKER_FLAGS:-} CMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE:-} if [[ "${ENABLE_WASM_SIMD}" -ne "0" ]]; then CMAKE_CXX_FLAGS="${CMAKE_CXX_FLAGS} -msimd128" CMAKE_C_FLAGS="${CMAKE_C_FLAGS} -msimd128" CMAKE_EXE_LINKER_FLAGS="${CMAKE_EXE_LINKER_FLAGS} -msimd128" fi if [[ ! -z "${HWY_BASELINE_TARGETS}" ]]; then CMAKE_CXX_FLAGS="${CMAKE_CXX_FLAGS} -DHWY_BASELINE_TARGETS=${HWY_BASELINE_TARGETS}" fi # Version inferred from the CI variables. CI_COMMIT_SHA=${CI_COMMIT_SHA:-${GITHUB_SHA:-}} JPEGXL_VERSION=${JPEGXL_VERSION:-${CI_COMMIT_SHA:0:8}} # Benchmark parameters STORE_IMAGES=${STORE_IMAGES:-1} BENCHMARK_CORPORA="${MYDIR}/third_party/corpora" # Local flags passed to sanitizers. UBSAN_FLAGS=( -fsanitize=alignment -fsanitize=bool -fsanitize=bounds -fsanitize=builtin -fsanitize=enum -fsanitize=float-cast-overflow -fsanitize=float-divide-by-zero -fsanitize=integer-divide-by-zero -fsanitize=null -fsanitize=object-size -fsanitize=pointer-overflow -fsanitize=return -fsanitize=returns-nonnull-attribute -fsanitize=shift-base -fsanitize=shift-exponent -fsanitize=unreachable -fsanitize=vla-bound -fno-sanitize-recover=undefined # Brunsli uses unaligned accesses to uint32_t, so alignment is just a warning. -fsanitize-recover=alignment ) # -fsanitize=function doesn't work on aarch64 and arm. if [[ "${BUILD_TARGET%%-*}" != "aarch64" && "${BUILD_TARGET%%-*}" != "arm" ]]; then UBSAN_FLAGS+=( -fsanitize=function ) fi if [[ "${BUILD_TARGET%%-*}" != "arm" ]]; then UBSAN_FLAGS+=( -fsanitize=signed-integer-overflow ) fi CLANG_TIDY_BIN=$(which clang-tidy-6.0 clang-tidy-7 clang-tidy-8 clang-tidy | head -n 1) # Default to "cat" if "colordiff" is not installed or if stdout is not a tty. if [[ -t 1 ]]; then COLORDIFF_BIN=$(which colordiff cat | head -n 1) else COLORDIFF_BIN="cat" fi FIND_BIN=$(which gfind find | head -n 1) # "false" will disable wine64 when not installed. This won't allow # cross-compiling. WINE_BIN=$(which wine64 false | head -n 1) CLANG_VERSION="${CLANG_VERSION:-}" # Detect the clang version suffix and store it in CLANG_VERSION. For example, # "6.0" for clang 6 or "7" for clang 7. detect_clang_version() { if [[ -n "${CLANG_VERSION}" ]]; then return 0 fi local clang_version=$("${CC:-clang}" --version | head -n1) clang_version=${clang_version#"Debian "} local llvm_tag case "${clang_version}" in "clang version 6."*) CLANG_VERSION="6.0" ;; "clang version "*) # Any other clang version uses just the major version number. local suffix="${clang_version#clang version }" CLANG_VERSION="${suffix%%.*}" ;; "emcc"*) # We can't use asan or msan in the emcc case. ;; *) echo "Unknown clang version: ${clang_version}" >&2 return 1 esac } # Temporary files cleanup hooks. CLEANUP_FILES=() cleanup() { if [[ ${#CLEANUP_FILES[@]} -ne 0 ]]; then rm -fr "${CLEANUP_FILES[@]}" fi } # Executed on exit. on_exit() { local retcode="$1" # Always cleanup the CLEANUP_FILES. cleanup # Post a message in the MR when requested with POST_MESSAGE_ON_ERROR but only # if the run failed and we are not running from a MR pipeline. if [[ ${retcode} -ne 0 && -n "${CI_BUILD_NAME:-}" && -n "${POST_MESSAGE_ON_ERROR}" && -z "${CI_MERGE_REQUEST_ID:-}" && "${CI_BUILD_REF_NAME}" = "master" ]]; then load_mr_vars_from_commit { set +xeu; } 2>/dev/null local message="**Run ${CI_BUILD_NAME} @ ${CI_COMMIT_SHORT_SHA} failed.** Check the output of the job at ${CI_JOB_URL:-} to see if this was your problem. If it was, please rollback this change or fix the problem ASAP, broken builds slow down development. Check if the error already existed in the previous build as well. Pipeline: ${CI_PIPELINE_URL} Previous build commit: ${CI_COMMIT_BEFORE_SHA} " cmd_post_mr_comment "${message}" fi } trap 'retcode=$?; { set +x; } 2>/dev/null; on_exit ${retcode}' INT TERM EXIT # These variables are populated when calling merge_request_commits(). # The current hash at the top of the current branch or merge request branch (if # running from a merge request pipeline). MR_HEAD_SHA="" # The common ancestor between the current commit and the tracked branch, such # as master. This includes a list MR_ANCESTOR_SHA="" # Populate MR_HEAD_SHA and MR_ANCESTOR_SHA. merge_request_commits() { { set +x; } 2>/dev/null # GITHUB_SHA is the current reference being build in GitHub Actions. if [[ -n "${GITHUB_SHA:-}" ]]; then # GitHub normally does a checkout of a merge commit on a shallow repository # by default. We want to get a bit more of the history to be able to diff # changes on the Pull Request if needed. This fetches 10 more commits which # should be enough given that PR normally should have 1 commit. git -C "${MYDIR}" fetch -q origin "${GITHUB_SHA}" --depth 10 MR_HEAD_SHA="$(git rev-parse "FETCH_HEAD^2" 2>/dev/null || echo "${GITHUB_SHA}")" else # CI_BUILD_REF is the reference currently being build in the CI workflow. MR_HEAD_SHA=$(git -C "${MYDIR}" rev-parse -q "${CI_BUILD_REF:-HEAD}") fi if [[ -n "${CI_MERGE_REQUEST_IID:-}" ]]; then # Merge request pipeline in CI. In this case the upstream is called "origin" # but it refers to the forked project that's the source of the merge # request. We need to get the target of the merge request, for which we need # to query that repository using our CI_JOB_TOKEN. echo "machine gitlab.com login gitlab-ci-token password ${CI_JOB_TOKEN}" \ >> "${HOME}/.netrc" git -C "${MYDIR}" fetch "${CI_MERGE_REQUEST_PROJECT_URL}" \ "${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}" MR_ANCESTOR_SHA=$(git -C "${MYDIR}" rev-parse -q FETCH_HEAD) elif [[ -n "${GITHUB_BASE_REF:-}" ]]; then # Pull request workflow in GitHub Actions. GitHub checkout action uses # "origin" as the remote for the git checkout. git -C "${MYDIR}" fetch -q origin "${GITHUB_BASE_REF}" MR_ANCESTOR_SHA=$(git -C "${MYDIR}" rev-parse -q FETCH_HEAD) else # We are in a local branch, not a merge request. MR_ANCESTOR_SHA=$(git -C "${MYDIR}" rev-parse -q HEAD@{upstream} || true) fi if [[ -z "${MR_ANCESTOR_SHA}" ]]; then echo "Warning, not tracking any branch, using the last commit in HEAD.">&2 # This prints the return value with just HEAD. MR_ANCESTOR_SHA=$(git -C "${MYDIR}" rev-parse -q "${MR_HEAD_SHA}^") else # GitHub runs the pipeline on a merge commit, no need to look for the common # ancestor in that case. if [[ -z "${GITHUB_BASE_REF:-}" ]]; then MR_ANCESTOR_SHA=$(git -C "${MYDIR}" merge-base \ "${MR_ANCESTOR_SHA}" "${MR_HEAD_SHA}") fi fi set -x } # Load the MR iid from the landed commit message when running not from a # merge request workflow. This is useful to post back results at the merge # request when running pipelines from master. load_mr_vars_from_commit() { { set +x; } 2>/dev/null if [[ -z "${CI_MERGE_REQUEST_IID:-}" ]]; then local mr_iid=$(git rev-list --format=%B --max-count=1 HEAD | grep -F "${CI_PROJECT_URL}" | grep -F "/merge_requests" | head -n 1) # mr_iid contains a string like this if it matched: # Part-of: if [[ -n "${mr_iid}" ]]; then mr_iid=$(echo "${mr_iid}" | sed -E 's,^.*merge_requests/([0-9]+)>.*$,\1,') CI_MERGE_REQUEST_IID="${mr_iid}" CI_MERGE_REQUEST_PROJECT_ID=${CI_PROJECT_ID} fi fi set -x } # Posts a comment to the current merge request. cmd_post_mr_comment() { { set +x; } 2>/dev/null local comment="$1" if [[ -n "${BOT_TOKEN:-}" && -n "${CI_MERGE_REQUEST_IID:-}" ]]; then local url="${CI_API_V4_URL}/projects/${CI_MERGE_REQUEST_PROJECT_ID}/merge_requests/${CI_MERGE_REQUEST_IID}/notes" curl -X POST -g \ -H "PRIVATE-TOKEN: ${BOT_TOKEN}" \ --data-urlencode "body=${comment}" \ --output /dev/null \ "${url}" fi set -x } # Set up and export the environment variables needed by the child processes. export_env() { if [[ "${BUILD_TARGET}" == *mingw32 ]]; then # Wine needs to know the paths to the mingw dlls. These should be # separated by ';'. WINEPATH=$("${CC:-clang}" -print-search-dirs --target="${BUILD_TARGET}" \ | grep -F 'libraries: =' | cut -f 2- -d '=' | tr ':' ';') # We also need our own libraries in the wine path. local real_build_dir=$(realpath "${BUILD_DIR}") # Some library .dll dependencies are installed in /bin: export WINEPATH="${WINEPATH};${real_build_dir};${real_build_dir}/third_party/brotli;/usr/${BUILD_TARGET}/bin" local prefix="${BUILD_DIR}/wineprefix" mkdir -p "${prefix}" export WINEPREFIX=$(realpath "${prefix}") fi # Sanitizers need these variables to print and properly format the stack # traces: LLVM_SYMBOLIZER=$("${CC:-clang}" -print-prog-name=llvm-symbolizer || true) if [[ -n "${LLVM_SYMBOLIZER}" ]]; then export ASAN_SYMBOLIZER_PATH="${LLVM_SYMBOLIZER}" export MSAN_SYMBOLIZER_PATH="${LLVM_SYMBOLIZER}" export UBSAN_SYMBOLIZER_PATH="${LLVM_SYMBOLIZER}" fi } cmake_configure() { export_env if [[ "${STACK_SIZE:-0}" == 1 ]]; then # Dump the stack size of each function in the .stack_sizes section for # analysis. CMAKE_C_FLAGS+=" -fstack-size-section" CMAKE_CXX_FLAGS+=" -fstack-size-section" fi local args=( -B"${BUILD_DIR}" -H"${MYDIR}" -DCMAKE_BUILD_TYPE="${CMAKE_BUILD_TYPE}" -G Ninja -DCMAKE_CXX_FLAGS="${CMAKE_CXX_FLAGS}" -DCMAKE_C_FLAGS="${CMAKE_C_FLAGS}" -DCMAKE_EXE_LINKER_FLAGS="${CMAKE_EXE_LINKER_FLAGS}" -DCMAKE_MODULE_LINKER_FLAGS="${CMAKE_MODULE_LINKER_FLAGS}" -DCMAKE_SHARED_LINKER_FLAGS="${CMAKE_SHARED_LINKER_FLAGS}" -DJPEGXL_VERSION="${JPEGXL_VERSION}" -DSANITIZER="${SANITIZER}" # These are not enabled by default in cmake. -DJPEGXL_ENABLE_VIEWERS=ON -DJPEGXL_ENABLE_PLUGINS=ON -DJPEGXL_ENABLE_DEVTOOLS=ON # We always use libfuzzer in the ci.sh wrapper. -DJPEGXL_FUZZER_LINK_FLAGS="-fsanitize=fuzzer" ) if [[ "${BUILD_TARGET}" != *mingw32 ]]; then args+=( -DJPEGXL_WARNINGS_AS_ERRORS=ON ) fi if [[ -n "${BUILD_TARGET}" ]]; then local system_name="Linux" if [[ "${BUILD_TARGET}" == *mingw32 ]]; then # When cross-compiling with mingw the target must be set to Windows and # run programs with wine. system_name="Windows" args+=( -DCMAKE_CROSSCOMPILING_EMULATOR="${WINE_BIN}" # Normally CMake automatically defines MINGW=1 when building with the # mingw compiler (x86_64-w64-mingw32-gcc) but we are normally compiling # with clang. -DMINGW=1 ) fi # EMSCRIPTEN toolchain sets the right values itself if [[ "${BUILD_TARGET}" != wasm* ]]; then # If set, BUILD_TARGET must be the target triplet such as # x86_64-unknown-linux-gnu. args+=( -DCMAKE_C_COMPILER_TARGET="${BUILD_TARGET}" -DCMAKE_CXX_COMPILER_TARGET="${BUILD_TARGET}" # Only the first element of the target triplet. -DCMAKE_SYSTEM_PROCESSOR="${BUILD_TARGET%%-*}" -DCMAKE_SYSTEM_NAME="${system_name}" -DCMAKE_TOOLCHAIN_FILE="${CMAKE_TOOLCHAIN_FILE}" ) else args+=( # sjpeg confuses WASM SIMD with SSE. -DSJPEG_ENABLE_SIMD=OFF # Building shared libs is not very useful for WASM. -DBUILD_SHARED_LIBS=OFF ) fi args+=( # These are needed to make googletest work when cross-compiling. -DCMAKE_CROSSCOMPILING=1 -DHAVE_STD_REGEX=0 -DHAVE_POSIX_REGEX=0 -DHAVE_GNU_POSIX_REGEX=0 -DHAVE_STEADY_CLOCK=0 -DHAVE_THREAD_SAFETY_ATTRIBUTES=0 ) if [[ -z "${CMAKE_FIND_ROOT_PATH}" ]]; then # find_package() will look in this prefix for libraries. CMAKE_FIND_ROOT_PATH="/usr/${BUILD_TARGET}" fi if [[ -z "${CMAKE_PREFIX_PATH}" ]]; then CMAKE_PREFIX_PATH="/usr/${BUILD_TARGET}" fi # Use pkg-config for the target. If there's no pkg-config available for the # target we can set the PKG_CONFIG_PATH to the appropriate path in most # linux distributions. local pkg_config=$(which "${BUILD_TARGET}-pkg-config" || true) if [[ -z "${pkg_config}" ]]; then pkg_config=$(which pkg-config) export PKG_CONFIG_LIBDIR="/usr/${BUILD_TARGET}/lib/pkgconfig" fi if [[ -n "${pkg_config}" ]]; then args+=(-DPKG_CONFIG_EXECUTABLE="${pkg_config}") fi fi if [[ -n "${CMAKE_CROSSCOMPILING_EMULATOR}" ]]; then args+=( -DCMAKE_CROSSCOMPILING_EMULATOR="${CMAKE_CROSSCOMPILING_EMULATOR}" ) fi if [[ -n "${CMAKE_FIND_ROOT_PATH}" ]]; then args+=( -DCMAKE_FIND_ROOT_PATH="${CMAKE_FIND_ROOT_PATH}" ) fi if [[ -n "${CMAKE_PREFIX_PATH}" ]]; then args+=( -DCMAKE_PREFIX_PATH="${CMAKE_PREFIX_PATH}" ) fi if [[ -n "${CMAKE_C_COMPILER_LAUNCHER}" ]]; then args+=( -DCMAKE_C_COMPILER_LAUNCHER="${CMAKE_C_COMPILER_LAUNCHER}" ) fi if [[ -n "${CMAKE_CXX_COMPILER_LAUNCHER}" ]]; then args+=( -DCMAKE_CXX_COMPILER_LAUNCHER="${CMAKE_CXX_COMPILER_LAUNCHER}" ) fi if [[ -n "${CMAKE_MAKE_PROGRAM}" ]]; then args+=( -DCMAKE_MAKE_PROGRAM="${CMAKE_MAKE_PROGRAM}" ) fi if [[ "${BUILD_TARGET}" == wasm* ]]; then emcmake cmake "${args[@]}" "$@" else cmake "${args[@]}" "$@" fi } cmake_build_and_test() { # gtest_discover_tests() runs the test binaries to discover the list of tests # at build time, which fails under qemu. ASAN_OPTIONS=detect_leaks=0 cmake --build "${BUILD_DIR}" -- all doc # Pack test binaries if requested. if [[ "${PACK_TEST:-}" == "1" ]]; then (cd "${BUILD_DIR}" ${FIND_BIN} -name '*.cmake' -a '!' -path '*CMakeFiles*' # gtest / gmock / gtest_main shared libs ${FIND_BIN} lib/ -name 'libg*.so*' ${FIND_BIN} -type d -name tests -a '!' -path '*CMakeFiles*' ) | tar -C "${BUILD_DIR}" -cf "${BUILD_DIR}/tests.tar.xz" -T - \ --use-compress-program="xz --threads=$(nproc --all || echo 1) -6" du -h "${BUILD_DIR}/tests.tar.xz" # Pack coverage data if also available. touch "${BUILD_DIR}/gcno.sentinel" (cd "${BUILD_DIR}"; echo gcno.sentinel; ${FIND_BIN} -name '*gcno') | \ tar -C "${BUILD_DIR}" -cvf "${BUILD_DIR}/gcno.tar.xz" -T - \ --use-compress-program="xz --threads=$(nproc --all || echo 1) -6" fi if [[ "${SKIP_TEST}" -ne "1" ]]; then (cd "${BUILD_DIR}" export UBSAN_OPTIONS=print_stacktrace=1 [[ "${TEST_STACK_LIMIT}" == "none" ]] || ulimit -s "${TEST_STACK_LIMIT}" ctest -j $(nproc --all || echo 1) ${TEST_SELECTOR} --output-on-failure) fi } # Configure the build to strip unused functions. This considerably reduces the # output size, specially for tests which only use a small part of the whole # library. strip_dead_code() { # Emscripten does tree shaking without any extra flags. if [[ "${BUILD_TARGET}" == wasm* ]]; then return 0 fi # -ffunction-sections, -fdata-sections and -Wl,--gc-sections effectively # discard all unreachable code, reducing the code size. For this to work, we # need to also pass --no-export-dynamic to prevent it from exporting all the # internal symbols (like functions) making them all reachable and thus not a # candidate for removal. CMAKE_CXX_FLAGS+=" -ffunction-sections -fdata-sections" CMAKE_C_FLAGS+=" -ffunction-sections -fdata-sections" if [[ "${OS}" == "Darwin" ]]; then CMAKE_EXE_LINKER_FLAGS+=" -dead_strip" CMAKE_SHARED_LINKER_FLAGS+=" -dead_strip" else CMAKE_EXE_LINKER_FLAGS+=" -Wl,--gc-sections -Wl,--no-export-dynamic" CMAKE_SHARED_LINKER_FLAGS+=" -Wl,--gc-sections -Wl,--no-export-dynamic" fi } ### Externally visible commands cmd_debug() { CMAKE_BUILD_TYPE="Debug" cmake_configure "$@" cmake_build_and_test } cmd_release() { CMAKE_BUILD_TYPE="Release" strip_dead_code cmake_configure "$@" cmake_build_and_test } cmd_opt() { CMAKE_BUILD_TYPE="RelWithDebInfo" CMAKE_CXX_FLAGS+=" -DJXL_DEBUG_WARNING -DJXL_DEBUG_ON_ERROR" cmake_configure "$@" cmake_build_and_test } cmd_coverage() { # -O0 prohibits stack space reuse -> causes stack-overflow on dozens of tests. TEST_STACK_LIMIT="none" cmd_release -DJPEGXL_ENABLE_COVERAGE=ON "$@" if [[ "${SKIP_TEST}" -ne "1" ]]; then # If we didn't run the test we also don't print a coverage report. cmd_coverage_report fi } cmd_coverage_report() { LLVM_COV=$("${CC:-clang}" -print-prog-name=llvm-cov) local real_build_dir=$(realpath "${BUILD_DIR}") local gcovr_args=( -r "${real_build_dir}" --gcov-executable "${LLVM_COV} gcov" # Only print coverage information for the libjxl directories. The rest # is not part of the code under test. --filter '.*jxl/.*' --exclude '.*_test.cc' --exclude '.*_testonly..*' --exclude '.*_debug.*' --exclude '.*test_utils..*' --object-directory "${real_build_dir}" ) ( cd "${real_build_dir}" gcovr "${gcovr_args[@]}" --html --html-details \ --output="${real_build_dir}/coverage.html" gcovr "${gcovr_args[@]}" --print-summary | tee "${real_build_dir}/coverage.txt" gcovr "${gcovr_args[@]}" --xml --output="${real_build_dir}/coverage.xml" ) } cmd_test() { export_env # Unpack tests if needed. if [[ -e "${BUILD_DIR}/tests.tar.xz" && ! -d "${BUILD_DIR}/tests" ]]; then tar -C "${BUILD_DIR}" -Jxvf "${BUILD_DIR}/tests.tar.xz" fi if [[ -e "${BUILD_DIR}/gcno.tar.xz" && ! -d "${BUILD_DIR}/gcno.sentinel" ]]; then tar -C "${BUILD_DIR}" -Jxvf "${BUILD_DIR}/gcno.tar.xz" fi (cd "${BUILD_DIR}" export UBSAN_OPTIONS=print_stacktrace=1 [[ "${TEST_STACK_LIMIT}" == "none" ]] || ulimit -s "${TEST_STACK_LIMIT}" ctest -j $(nproc --all || echo 1) --output-on-failure "$@") } cmd_gbench() { export_env (cd "${BUILD_DIR}" export UBSAN_OPTIONS=print_stacktrace=1 lib/jxl_gbench \ --benchmark_counters_tabular=true \ --benchmark_out_format=json \ --benchmark_out=gbench.json "$@" ) } cmd_asanfuzz() { CMAKE_CXX_FLAGS+=" -fsanitize=fuzzer-no-link -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1" CMAKE_C_FLAGS+=" -fsanitize=fuzzer-no-link -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1" cmd_asan -DJPEGXL_ENABLE_FUZZERS=ON "$@" } cmd_msanfuzz() { # Install msan if needed before changing the flags. detect_clang_version local msan_prefix="${HOME}/.msan/${CLANG_VERSION}" if [[ ! -d "${msan_prefix}" || -e "${msan_prefix}/lib/libc++abi.a" ]]; then # Install msan libraries for this version if needed or if an older version # with libc++abi was installed. cmd_msan_install fi CMAKE_CXX_FLAGS+=" -fsanitize=fuzzer-no-link -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1" CMAKE_C_FLAGS+=" -fsanitize=fuzzer-no-link -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1" cmd_msan -DJPEGXL_ENABLE_FUZZERS=ON "$@" } cmd_asan() { SANITIZER="asan" CMAKE_C_FLAGS+=" -DJXL_ENABLE_ASSERT=1 -g -DADDRESS_SANITIZER \ -fsanitize=address ${UBSAN_FLAGS[@]}" CMAKE_CXX_FLAGS+=" -DJXL_ENABLE_ASSERT=1 -g -DADDRESS_SANITIZER \ -fsanitize=address ${UBSAN_FLAGS[@]}" strip_dead_code cmake_configure "$@" -DJPEGXL_ENABLE_TCMALLOC=OFF cmake_build_and_test } cmd_tsan() { SANITIZER="tsan" local tsan_args=( -DJXL_ENABLE_ASSERT=1 -g -DTHREAD_SANITIZER ${UBSAN_FLAGS[@]} -fsanitize=thread ) CMAKE_C_FLAGS+=" ${tsan_args[@]}" CMAKE_CXX_FLAGS+=" ${tsan_args[@]}" CMAKE_BUILD_TYPE="RelWithDebInfo" cmake_configure "$@" -DJPEGXL_ENABLE_TCMALLOC=OFF cmake_build_and_test } cmd_msan() { SANITIZER="msan" detect_clang_version local msan_prefix="${HOME}/.msan/${CLANG_VERSION}" if [[ ! -d "${msan_prefix}" || -e "${msan_prefix}/lib/libc++abi.a" ]]; then # Install msan libraries for this version if needed or if an older version # with libc++abi was installed. cmd_msan_install fi local msan_c_flags=( -fsanitize=memory -fno-omit-frame-pointer -fsanitize-memory-track-origins -DJXL_ENABLE_ASSERT=1 -g -DMEMORY_SANITIZER # Force gtest to not use the cxxbai. -DGTEST_HAS_CXXABI_H_=0 ) local msan_cxx_flags=( "${msan_c_flags[@]}" # Some C++ sources don't use the std at all, so the -stdlib=libc++ is unused # in those cases. Ignore the warning. -Wno-unused-command-line-argument -stdlib=libc++ # We include the libc++ from the msan directory instead, so we don't want # the std includes. -nostdinc++ -cxx-isystem"${msan_prefix}/include/c++/v1" ) local msan_linker_flags=( -L"${msan_prefix}"/lib -Wl,-rpath -Wl,"${msan_prefix}"/lib/ ) CMAKE_C_FLAGS+=" ${msan_c_flags[@]} ${UBSAN_FLAGS[@]}" CMAKE_CXX_FLAGS+=" ${msan_cxx_flags[@]} ${UBSAN_FLAGS[@]}" CMAKE_EXE_LINKER_FLAGS+=" ${msan_linker_flags[@]}" CMAKE_MODULE_LINKER_FLAGS+=" ${msan_linker_flags[@]}" CMAKE_SHARED_LINKER_FLAGS+=" ${msan_linker_flags[@]}" strip_dead_code cmake_configure "$@" \ -DCMAKE_CROSSCOMPILING=1 -DRUN_HAVE_STD_REGEX=0 -DRUN_HAVE_POSIX_REGEX=0 \ -DJPEGXL_ENABLE_TCMALLOC=OFF -DJPEGXL_WARNINGS_AS_ERRORS=OFF \ -DCMAKE_REQUIRED_LINK_OPTIONS="${msan_linker_flags[@]}" cmake_build_and_test } # Install libc++ libraries compiled with msan in the msan_prefix for the current # compiler version. cmd_msan_install() { local tmpdir=$(mktemp -d) CLEANUP_FILES+=("${tmpdir}") # Detect the llvm to install: export CC="${CC:-clang}" export CXX="${CXX:-clang++}" detect_clang_version local llvm_tag="llvmorg-${CLANG_VERSION}.0.0" case "${CLANG_VERSION}" in "6.0") llvm_tag="llvmorg-6.0.1" ;; "7") llvm_tag="llvmorg-7.0.1" ;; esac local llvm_targz="${tmpdir}/${llvm_tag}.tar.gz" curl -L --show-error -o "${llvm_targz}" \ "https://github.com/llvm/llvm-project/archive/${llvm_tag}.tar.gz" tar -C "${tmpdir}" -zxf "${llvm_targz}" local llvm_root="${tmpdir}/llvm-project-${llvm_tag}" local msan_prefix="${HOME}/.msan/${CLANG_VERSION}" rm -rf "${msan_prefix}" declare -A CMAKE_EXTRAS CMAKE_EXTRAS[libcxx]="\ -DLIBCXX_CXX_ABI=libstdc++ \ -DLIBCXX_INSTALL_EXPERIMENTAL_LIBRARY=ON" for project in libcxx; do local proj_build="${tmpdir}/build-${project}" local proj_dir="${llvm_root}/${project}" mkdir -p "${proj_build}" cmake -B"${proj_build}" -H"${proj_dir}" \ -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ -DLLVM_USE_SANITIZER=Memory \ -DLLVM_PATH="${llvm_root}/llvm" \ -DLLVM_CONFIG_PATH="$(which llvm-config llvm-config-7 llvm-config-6.0 | \ head -n1)" \ -DCMAKE_CXX_FLAGS="${CMAKE_CXX_FLAGS}" \ -DCMAKE_C_FLAGS="${CMAKE_C_FLAGS}" \ -DCMAKE_EXE_LINKER_FLAGS="${CMAKE_EXE_LINKER_FLAGS}" \ -DCMAKE_SHARED_LINKER_FLAGS="${CMAKE_SHARED_LINKER_FLAGS}" \ -DCMAKE_INSTALL_PREFIX="${msan_prefix}" \ ${CMAKE_EXTRAS[${project}]} cmake --build "${proj_build}" ninja -C "${proj_build}" install done } # Internal build step shared between all cmd_ossfuzz_* commands. _cmd_ossfuzz() { local sanitizer="$1" shift mkdir -p "${BUILD_DIR}" local real_build_dir=$(realpath "${BUILD_DIR}") # oss-fuzz defines three directories: # * /work, with the working directory to do re-builds # * /src, with the source code to build # * /out, with the output directory where to copy over the built files. # We use $BUILD_DIR as the /work and the script directory as the /src. The # /out directory is ignored as developers are used to look for the fuzzers in # $BUILD_DIR/tools/ directly. if [[ "${sanitizer}" = "memory" && ! -d "${BUILD_DIR}/msan" ]]; then sudo docker run --rm -i \ --user $(id -u):$(id -g) \ -v "${real_build_dir}":/work \ gcr.io/oss-fuzz-base/msan-libs-builder \ bash -c "cp -r /msan /work" fi # Args passed to ninja. These will be evaluated as a string separated by # spaces. local jpegxl_extra_args="$@" sudo docker run --rm -i \ -e JPEGXL_UID=$(id -u) \ -e JPEGXL_GID=$(id -g) \ -e FUZZING_ENGINE="${FUZZING_ENGINE:-libfuzzer}" \ -e SANITIZER="${sanitizer}" \ -e ARCHITECTURE=x86_64 \ -e FUZZING_LANGUAGE=c++ \ -e MSAN_LIBS_PATH="/work/msan" \ -e JPEGXL_EXTRA_ARGS="${jpegxl_extra_args}" \ -v "${MYDIR}":/src/libjxl \ -v "${MYDIR}/tools/ossfuzz-build.sh":/src/build.sh \ -v "${real_build_dir}":/work \ gcr.io/oss-fuzz/libjxl } cmd_ossfuzz_asan() { _cmd_ossfuzz address "$@" } cmd_ossfuzz_msan() { _cmd_ossfuzz memory "$@" } cmd_ossfuzz_ubsan() { _cmd_ossfuzz undefined "$@" } cmd_ossfuzz_ninja() { [[ -e "${BUILD_DIR}/build.ninja" ]] local real_build_dir=$(realpath "${BUILD_DIR}") if [[ -e "${BUILD_DIR}/msan" ]]; then echo "ossfuzz_ninja doesn't work with msan builds. Use ossfuzz_msan." >&2 exit 1 fi sudo docker run --rm -i \ --user $(id -u):$(id -g) \ -v "${MYDIR}":/src/libjxl \ -v "${real_build_dir}":/work \ gcr.io/oss-fuzz/libjxl \ ninja -C /work "$@" } cmd_fast_benchmark() { local small_corpus_tar="${BENCHMARK_CORPORA}/jyrki-full.tar" mkdir -p "${BENCHMARK_CORPORA}" curl --show-error -o "${small_corpus_tar}" -z "${small_corpus_tar}" \ "https://storage.googleapis.com/artifacts.jpegxl.appspot.com/corpora/jyrki-full.tar" local tmpdir=$(mktemp -d) CLEANUP_FILES+=("${tmpdir}") tar -xf "${small_corpus_tar}" -C "${tmpdir}" run_benchmark "${tmpdir}" 1048576 } cmd_benchmark() { local nikon_corpus_tar="${BENCHMARK_CORPORA}/nikon-subset.tar" mkdir -p "${BENCHMARK_CORPORA}" curl --show-error -o "${nikon_corpus_tar}" -z "${nikon_corpus_tar}" \ "https://storage.googleapis.com/artifacts.jpegxl.appspot.com/corpora/nikon-subset.tar" local tmpdir=$(mktemp -d) CLEANUP_FILES+=("${tmpdir}") tar -xvf "${nikon_corpus_tar}" -C "${tmpdir}" local sem_id="jpegxl_benchmark-$$" local nprocs=$(nproc --all || echo 1) images=() local filename while IFS= read -r filename; do # This removes the './' filename="${filename:2}" local mode if [[ "${filename:0:4}" == "srgb" ]]; then mode="RGB_D65_SRG_Rel_SRG" elif [[ "${filename:0:5}" == "adobe" ]]; then mode="RGB_D65_Ado_Rel_Ado" else echo "Unknown image colorspace: ${filename}" >&2 exit 1 fi png_filename="${filename%.ppm}.png" png_filename=$(echo "${png_filename}" | tr '/' '_') sem --bg --id "${sem_id}" -j"${nprocs}" -- \ "${BUILD_DIR}/tools/decode_and_encode" \ "${tmpdir}/${filename}" "${mode}" "${tmpdir}/${png_filename}" images+=( "${png_filename}" ) done < <(cd "${tmpdir}"; ${FIND_BIN} . -name '*.ppm' -type f) sem --id "${sem_id}" --wait # We need about 10 GiB per thread on these images. run_benchmark "${tmpdir}" 10485760 } get_mem_available() { if [[ "${OS}" == "Darwin" ]]; then echo $(vm_stat | grep -F 'Pages free:' | awk '{print $3 * 4}') else echo $(grep -F MemAvailable: /proc/meminfo | awk '{print $2}') fi } run_benchmark() { local src_img_dir="$1" local mem_per_thread="${2:-10485760}" local output_dir="${BUILD_DIR}/benchmark_results" mkdir -p "${output_dir}" # The memory available at the beginning of the benchmark run in kB. The number # of threads depends on the available memory, and the passed memory per # thread. We also add a 2 GiB of constant memory. local mem_available="$(get_mem_available)" # Check that we actually have a MemAvailable value. [[ -n "${mem_available}" ]] local num_threads=$(( (${mem_available} - 1048576) / ${mem_per_thread} )) if [[ ${num_threads} -le 0 ]]; then num_threads=1 fi local benchmark_args=( --input "${src_img_dir}/*.png" --codec=jpeg:yuv420:q85,webp:q80,jxl:d1:6,jxl:d1:6:downsampling=8,jxl:d5:6,jxl:d5:6:downsampling=8,jxl:m:d0:2,jxl:m:d0:3,jxl:m:d2:2 --output_dir "${output_dir}" --noprofiler --show_progress --num_threads="${num_threads}" ) if [[ "${STORE_IMAGES}" == "1" ]]; then benchmark_args+=(--save_decompressed --save_compressed) fi ( [[ "${TEST_STACK_LIMIT}" == "none" ]] || ulimit -s "${TEST_STACK_LIMIT}" "${BUILD_DIR}/tools/benchmark_xl" "${benchmark_args[@]}" | \ tee "${output_dir}/results.txt" # Check error code for benckmark_xl command. This will exit if not. return ${PIPESTATUS[0]} ) if [[ -n "${CI_BUILD_NAME:-}" ]]; then { set +x; } 2>/dev/null local message="Results for ${CI_BUILD_NAME} @ ${CI_COMMIT_SHORT_SHA} (job ${CI_JOB_URL:-}): $(cat "${output_dir}/results.txt") " cmd_post_mr_comment "${message}" set -x fi } # Helper function to wait for the CPU temperature to cool down on ARM. wait_for_temp() { { set +x; } 2>/dev/null local temp_limit=${1:-38000} if [[ -z "${THERMAL_FILE:-}" ]]; then echo "Must define the THERMAL_FILE with the thermal_zoneX/temp file" \ "to read the temperature from. This is normally set in the runner." >&2 exit 1 fi local org_temp=$(cat "${THERMAL_FILE}") if [[ "${org_temp}" -ge "${temp_limit}" ]]; then echo -n "Waiting for temp to get down from ${org_temp}... " fi local temp="${org_temp}" local secs=0 while [[ "${temp}" -ge "${temp_limit}" ]]; do sleep 1 temp=$(cat "${THERMAL_FILE}") echo -n "${temp} " secs=$((secs + 1)) if [[ ${secs} -ge 5 ]]; then break fi done if [[ "${org_temp}" -ge "${temp_limit}" ]]; then echo "Done, temp=${temp}" fi set -x } # Helper function to set the cpuset restriction of the current process. cmd_cpuset() { [[ "${SKIP_CPUSET:-}" != "1" ]] || return 0 local newset="$1" local mycpuset=$(cat /proc/self/cpuset) mycpuset="/dev/cpuset${mycpuset}" # Check that the directory exists: [[ -d "${mycpuset}" ]] if [[ -e "${mycpuset}/cpuset.cpus" ]]; then echo "${newset}" >"${mycpuset}/cpuset.cpus" else echo "${newset}" >"${mycpuset}/cpus" fi } # Return the encoding/decoding speed from the Stats output. _speed_from_output() { local speed="$1" local unit="${2:-MP/s}" if [[ "${speed}" == *"${unit}"* ]]; then speed="${speed%% ${unit}*}" speed="${speed##* }" echo "${speed}" fi } # Run benchmarks on ARM for the big and little CPUs. cmd_arm_benchmark() { # Flags used for cjxl encoder with .png inputs local jxl_png_benchmarks=( # Lossy options: "--epf=0 --distance=1.0 --speed=cheetah" "--epf=2 --distance=1.0 --speed=cheetah" "--epf=0 --distance=8.0 --speed=cheetah" "--epf=1 --distance=8.0 --speed=cheetah" "--epf=2 --distance=8.0 --speed=cheetah" "--epf=3 --distance=8.0 --speed=cheetah" "--modular -Q 90" "--modular -Q 50" # Lossless options: "--modular" "--modular -E 0 -I 0" "--modular -P 5" "--modular --responsive=1" # Near-lossless options: "--epf=0 --distance=0.3 --speed=fast" "--modular -Q 97" ) # Flags used for cjxl encoder with .jpg inputs. These should do lossless # JPEG recompression (of pixels or full jpeg). local jxl_jpeg_benchmarks=( "--num_reps=3" ) local images=( "testdata/jxl/flower/flower.png" ) local jpg_images=( "testdata/jxl/flower/flower.png.im_q85_420.jpg" ) if [[ "${SKIP_CPUSET:-}" == "1" ]]; then # Use a single cpu config in this case. local cpu_confs=("?") else # Otherwise the CPU config comes from the environment: local cpu_confs=( "${RUNNER_CPU_LITTLE}" "${RUNNER_CPU_BIG}" # The CPU description is something like 3-7, so these configurations only # take the first CPU of the group. "${RUNNER_CPU_LITTLE%%-*}" "${RUNNER_CPU_BIG%%-*}" ) # Check that RUNNER_CPU_ALL is defined. In the SKIP_CPUSET=1 case this will # be ignored but still evaluated when calling cmd_cpuset. [[ -n "${RUNNER_CPU_ALL}" ]] fi local jpg_dirname="third_party/corpora/jpeg" mkdir -p "${jpg_dirname}" local jpg_qualities=( 50 80 95 ) for src_img in "${images[@]}"; do for q in "${jpg_qualities[@]}"; do local jpeg_name="${jpg_dirname}/"$(basename "${src_img}" .png)"-q${q}.jpg" convert -sampling-factor 1x1 -quality "${q}" \ "${src_img}" "${jpeg_name}" jpg_images+=("${jpeg_name}") done done local output_dir="${BUILD_DIR}/benchmark_results" mkdir -p "${output_dir}" local runs_file="${output_dir}/runs.txt" if [[ ! -e "${runs_file}" ]]; then echo -e "binary\tflags\tsrc_img\tsrc size\tsrc pixels\tcpuset\tenc size (B)\tenc speed (MP/s)\tdec speed (MP/s)\tJPG dec speed (MP/s)\tJPG dec speed (MB/s)" | tee -a "${runs_file}" fi mkdir -p "${BUILD_DIR}/arm_benchmark" local flags local src_img for src_img in "${jpg_images[@]}" "${images[@]}"; do local src_img_hash=$(sha1sum "${src_img}" | cut -f 1 -d ' ') local enc_binaries=("${BUILD_DIR}/tools/cjxl") local src_ext="${src_img##*.}" for enc_binary in "${enc_binaries[@]}"; do local enc_binary_base=$(basename "${enc_binary}") # Select the list of flags to use for the current encoder/image pair. local img_benchmarks if [[ "${src_ext}" == "jpg" ]]; then img_benchmarks=("${jxl_jpeg_benchmarks[@]}") else img_benchmarks=("${jxl_png_benchmarks[@]}") fi for flags in "${img_benchmarks[@]}"; do # Encoding step. local enc_file_hash="${enc_binary_base} || $flags || ${src_img} || ${src_img_hash}" enc_file_hash=$(echo "${enc_file_hash}" | sha1sum | cut -f 1 -d ' ') local enc_file="${BUILD_DIR}/arm_benchmark/${enc_file_hash}.jxl" for cpu_conf in "${cpu_confs[@]}"; do cmd_cpuset "${cpu_conf}" # nproc returns the number of active CPUs, which is given by the cpuset # mask. local num_threads="$(nproc)" echo "Encoding with: ${enc_binary_base} img=${src_img} cpus=${cpu_conf} enc_flags=${flags}" local enc_output if [[ "${flags}" == *"modular"* ]]; then # We don't benchmark encoding speed in this case. if [[ ! -f "${enc_file}" ]]; then cmd_cpuset "${RUNNER_CPU_ALL:-}" "${enc_binary}" ${flags} "${src_img}" "${enc_file}.tmp" mv "${enc_file}.tmp" "${enc_file}" cmd_cpuset "${cpu_conf}" fi enc_output=" ?? MP/s" else wait_for_temp enc_output=$("${enc_binary}" ${flags} "${src_img}" "${enc_file}.tmp" \ 2>&1 | tee /dev/stderr | grep -F "MP/s [") mv "${enc_file}.tmp" "${enc_file}" fi local enc_speed=$(_speed_from_output "${enc_output}") local enc_size=$(stat -c "%s" "${enc_file}") echo "Decoding with: img=${src_img} cpus=${cpu_conf} enc_flags=${flags}" local dec_output wait_for_temp dec_output=$("${BUILD_DIR}/tools/djxl" "${enc_file}" \ --num_reps=5 --num_threads="${num_threads}" 2>&1 | tee /dev/stderr | grep -E "M[BP]/s \[") local img_size=$(echo "${dec_output}" | cut -f 1 -d ',') local img_size_x=$(echo "${img_size}" | cut -f 1 -d ' ') local img_size_y=$(echo "${img_size}" | cut -f 3 -d ' ') local img_size_px=$(( ${img_size_x} * ${img_size_y} )) local dec_speed=$(_speed_from_output "${dec_output}") # For JPEG lossless recompression modes (where the original is a JPEG) # decode to JPG as well. local jpeg_dec_mps_speed="" local jpeg_dec_mbs_speed="" if [[ "${src_ext}" == "jpg" ]]; then wait_for_temp local dec_file="${BUILD_DIR}/arm_benchmark/${enc_file_hash}.jpg" dec_output=$("${BUILD_DIR}/tools/djxl" "${enc_file}" \ "${dec_file}" --num_reps=5 --num_threads="${num_threads}" 2>&1 | \ tee /dev/stderr | grep -E "M[BP]/s \[") local jpeg_dec_mps_speed=$(_speed_from_output "${dec_output}") local jpeg_dec_mbs_speed=$(_speed_from_output "${dec_output}" MB/s) if ! cmp --quiet "${src_img}" "${dec_file}"; then # Add a start at the end to signal that the files are different. jpeg_dec_mbs_speed+="*" fi fi # Record entry in a tab-separated file. local src_img_base=$(basename "${src_img}") echo -e "${enc_binary_base}\t${flags}\t${src_img_base}\t${img_size}\t${img_size_px}\t${cpu_conf}\t${enc_size}\t${enc_speed}\t${dec_speed}\t${jpeg_dec_mps_speed}\t${jpeg_dec_mbs_speed}" | tee -a "${runs_file}" done done done done cmd_cpuset "${RUNNER_CPU_ALL:-}" cat "${runs_file}" if [[ -n "${CI_BUILD_NAME:-}" ]]; then load_mr_vars_from_commit { set +x; } 2>/dev/null local message="Results for ${CI_BUILD_NAME} @ ${CI_COMMIT_SHORT_SHA} (job ${CI_JOB_URL:-}): \`\`\` $(column -t -s " " "${runs_file}") \`\`\` " cmd_post_mr_comment "${message}" set -x fi } # Generate a corpus and run the fuzzer on that corpus. cmd_fuzz() { local corpus_dir=$(realpath "${BUILD_DIR}/fuzzer_corpus") local fuzzer_crash_dir=$(realpath "${BUILD_DIR}/fuzzer_crash") mkdir -p "${corpus_dir}" "${fuzzer_crash_dir}" # Generate step. "${BUILD_DIR}/tools/fuzzer_corpus" "${corpus_dir}" # Run step: local nprocs=$(nproc --all || echo 1) ( cd "${BUILD_DIR}" "tools/djxl_fuzzer" "${fuzzer_crash_dir}" "${corpus_dir}" \ -max_total_time="${FUZZER_MAX_TIME}" -jobs=${nprocs} \ -artifact_prefix="${fuzzer_crash_dir}/" ) } # Runs the linter (clang-format) on the pending CLs. cmd_lint() { merge_request_commits { set +x; } 2>/dev/null local versions=(${1:-6.0 7 8 9 10 11}) local clang_format_bins=("${versions[@]/#/clang-format-}" clang-format) local tmpdir=$(mktemp -d) CLEANUP_FILES+=("${tmpdir}") local ret=0 local build_patch="${tmpdir}/build_cleaner.patch" if ! "${MYDIR}/tools/build_cleaner.py" >"${build_patch}"; then ret=1 echo "build_cleaner.py findings:" >&2 "${COLORDIFF_BIN}" <"${build_patch}" echo "Run \`tools/build_cleaner.py --update\` to apply them" >&2 fi local installed=() local clang_patch local clang_format for clang_format in "${clang_format_bins[@]}"; do if ! which "${clang_format}" >/dev/null; then continue fi installed+=("${clang_format}") local tmppatch="${tmpdir}/${clang_format}.patch" # We include in this linter all the changes including the uncommitted changes # to avoid printing changes already applied. set -x # Ignoring the error that git-clang-format outputs. git -C "${MYDIR}" "${clang_format}" --binary "${clang_format}" \ --style=file --diff "${MR_ANCESTOR_SHA}" -- >"${tmppatch}" || true { set +x; } 2>/dev/null if grep -E '^--- ' "${tmppatch}">/dev/null; then if [[ -n "${LINT_OUTPUT:-}" ]]; then cp "${tmppatch}" "${LINT_OUTPUT}" fi clang_patch="${tmppatch}" else echo "clang-format check OK" >&2 return ${ret} fi done if [[ ${#installed[@]} -eq 0 ]]; then echo "You must install clang-format for \"git clang-format\"" >&2 exit 1 fi # clang-format is installed but found problems. echo "clang-format findings:" >&2 "${COLORDIFF_BIN}" < "${clang_patch}" echo "clang-format found issues in your patches from ${MR_ANCESTOR_SHA}" \ "to the current patch. Run \`./ci.sh lint | patch -p1\` from the base" \ "directory to apply them." >&2 exit 1 } # Runs clang-tidy on the pending CLs. If the "all" argument is passed it runs # clang-tidy over all the source files instead. cmd_tidy() { local what="${1:-}" if [[ -z "${CLANG_TIDY_BIN}" ]]; then echo "ERROR: You must install clang-tidy-7 or newer to use ci.sh tidy" >&2 exit 1 fi local git_args=() if [[ "${what}" == "all" ]]; then git_args=(ls-files) shift else merge_request_commits git_args=( diff-tree --no-commit-id --name-only -r "${MR_ANCESTOR_SHA}" "${MR_HEAD_SHA}" ) fi # Clang-tidy needs the compilation database generated by cmake. if [[ ! -e "${BUILD_DIR}/compile_commands.json" ]]; then # Generate the build options in debug mode, since we need the debug asserts # enabled for the clang-tidy analyzer to use them. CMAKE_BUILD_TYPE="Debug" cmake_configure # Build the autogen targets to generate the .h files from the .ui files. local autogen_targets=( $(ninja -C "${BUILD_DIR}" -t targets | grep -F _autogen: | cut -f 1 -d :) ) if [[ ${#autogen_targets[@]} != 0 ]]; then ninja -C "${BUILD_DIR}" "${autogen_targets[@]}" fi fi cd "${MYDIR}" local nprocs=$(nproc --all || echo 1) local ret=0 if ! parallel -j"${nprocs}" --keep-order -- \ "${CLANG_TIDY_BIN}" -p "${BUILD_DIR}" -format-style=file -quiet "$@" {} \ < <(git "${git_args[@]}" | grep -E '(\.cc|\.cpp)$') \ >"${BUILD_DIR}/clang-tidy.txt"; then ret=1 fi { set +x; } 2>/dev/null echo "Findings statistics:" >&2 grep -E ' \[[A-Za-z\.,\-]+\]' -o "${BUILD_DIR}/clang-tidy.txt" | sort \ | uniq -c >&2 if [[ $ret -ne 0 ]]; then cat >&2 </dev/null local debsdir="${BUILD_DIR}/debs" local f while IFS='' read -r -d '' f; do echo "=====================================================================" echo "Package $f:" dpkg --info $f dpkg --contents $f done < <(find "${BUILD_DIR}/debs" -maxdepth 1 -mindepth 1 -type f \ -name '*.deb' -print0) } build_debian_pkg() { local srcdir="$1" local srcpkg="$2" local debsdir="${BUILD_DIR}/debs" local builddir="${debsdir}/${srcpkg}" # debuild doesn't have an easy way to build out of tree, so we make a copy # of with all symlinks on the first level. mkdir -p "${builddir}" for f in $(find "${srcdir}" -mindepth 1 -maxdepth 1 -printf '%P\n'); do if [[ ! -L "${builddir}/$f" ]]; then rm -f "${builddir}/$f" ln -s "${srcdir}/$f" "${builddir}/$f" fi done ( cd "${builddir}" debuild -b -uc -us ) } cmd_debian_build() { local srcpkg="${1:-}" case "${srcpkg}" in jpeg-xl) build_debian_pkg "${MYDIR}" "jpeg-xl" ;; highway) build_debian_pkg "${MYDIR}/third_party/highway" "highway" ;; *) echo "ERROR: Must pass a valid source package name to build." >&2 ;; esac } get_version() { local varname=$1 local line=$(grep -F "set(${varname} " lib/CMakeLists.txt | head -n 1) [[ -n "${line}" ]] line="${line#set(${varname} }" line="${line%)}" echo "${line}" } cmd_bump_version() { local newver="${1:-}" if ! which dch >/dev/null; then echo "Run:\n sudo apt install debhelper" exit 1 fi if [[ -z "${newver}" ]]; then local major=$(get_version JPEGXL_MAJOR_VERSION) local minor=$(get_version JPEGXL_MINOR_VERSION) local patch=0 minor=$(( ${minor} + 1)) else local major="${newver%%.*}" newver="${newver#*.}" local minor="${newver%%.*}" newver="${newver#${minor}}" local patch="${newver#.}" if [[ -z "${patch}" ]]; then patch=0 fi fi newver="${major}.${minor}" if [[ "${patch}" != "0" ]]; then newver="${newver}.${patch}" fi echo "Bumping version to ${newver} (${major}.${minor}.${patch})" sed -E \ -e "s/(set\\(JPEGXL_MAJOR_VERSION) [0-9]+\\)/\\1 ${major})/" \ -e "s/(set\\(JPEGXL_MINOR_VERSION) [0-9]+\\)/\\1 ${minor})/" \ -e "s/(set\\(JPEGXL_PATCH_VERSION) [0-9]+\\)/\\1 ${patch})/" \ -i lib/CMakeLists.txt # Update lib.gni tools/build_cleaner.py --update # Mark the previous version as "unstable". DEBCHANGE_RELEASE_HEURISTIC=log dch -M --distribution unstable --release '' DEBCHANGE_RELEASE_HEURISTIC=log dch -M \ --newversion "${newver}" \ "Bump JPEG XL version to ${newver}." } # Check that the AUTHORS file contains the email of the committer. cmd_authors() { merge_request_commits local emails local names readarray -t emails < <(git log --format='%ae' "${MR_HEAD_SHA}...${MR_ANCESTOR_SHA}") readarray -t names < <(git log --format='%an' "${MR_HEAD_SHA}...${MR_ANCESTOR_SHA}") for i in "${!names[@]}"; do echo "Checking name '${names[$i]}' with email '${emails[$i]}' ..." "${MYDIR}"/tools/check_author.py "${emails[$i]}" "${names[$i]}" done } main() { local cmd="${1:-}" if [[ -z "${cmd}" ]]; then cat >&2 < Build the given source package. debian_stats Print stats about the built packages. oss-fuzz commands: ossfuzz_asan Build the local source inside oss-fuzz docker with asan. ossfuzz_msan Build the local source inside oss-fuzz docker with msan. ossfuzz_ubsan Build the local source inside oss-fuzz docker with ubsan. ossfuzz_ninja Run ninja on the BUILD_DIR inside the oss-fuzz docker. Extra parameters are passed to ninja, for example "djxl_fuzzer" will only build that ninja target. Use for faster build iteration after one of the ossfuzz_*san commands. You can pass some optional environment variables as well: - BUILD_DIR: The output build directory (by default "$$repo/build") - BUILD_TARGET: The target triplet used when cross-compiling. - CMAKE_FLAGS: Convenience flag to pass both CMAKE_C_FLAGS and CMAKE_CXX_FLAGS. - CMAKE_PREFIX_PATH: Installation prefixes to be searched by the find_package. - ENABLE_WASM_SIMD=1: enable experimental SIMD in WASM build (only). - FUZZER_MAX_TIME: "fuzz" command fuzzer running timeout in seconds. - LINT_OUTPUT: Path to the output patch from the "lint" command. - SKIP_CPUSET=1: Skip modifying the cpuset in the arm_benchmark. - SKIP_TEST=1: Skip the test stage. - STORE_IMAGES=0: Makes the benchmark discard the computed images. - TEST_STACK_LIMIT: Stack size limit (ulimit -s) during tests, in KiB. - STACK_SIZE=1: Generate binaries with the .stack_sizes sections. These optional environment variables are forwarded to the cmake call as parameters: - CMAKE_BUILD_TYPE - CMAKE_C_FLAGS - CMAKE_CXX_FLAGS - CMAKE_C_COMPILER_LAUNCHER - CMAKE_CXX_COMPILER_LAUNCHER - CMAKE_CROSSCOMPILING_EMULATOR - CMAKE_FIND_ROOT_PATH - CMAKE_EXE_LINKER_FLAGS - CMAKE_MAKE_PROGRAM - CMAKE_MODULE_LINKER_FLAGS - CMAKE_SHARED_LINKER_FLAGS - CMAKE_TOOLCHAIN_FILE Example: BUILD_DIR=/tmp/build $0 opt EOF exit 1 fi cmd="cmd_${cmd}" shift set -x "${cmd}" "$@" } main "$@"