1#!/usr/bin/env bash 2# Copyright (c) the JPEG XL Project Authors. All rights reserved. 3# 4# Use of this source code is governed by a BSD-style 5# license that can be found in the LICENSE file. 6 7# Continuous integration helper module. This module is meant to be called from 8# the .gitlab-ci.yml file during the continuous integration build, as well as 9# from the command line for developers. 10 11set -eu 12 13OS=`uname -s` 14 15MYDIR=$(dirname $(realpath "$0")) 16 17### Environment parameters: 18TEST_STACK_LIMIT="${TEST_STACK_LIMIT:-128}" 19CMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE:-RelWithDebInfo} 20CMAKE_PREFIX_PATH=${CMAKE_PREFIX_PATH:-} 21CMAKE_C_COMPILER_LAUNCHER=${CMAKE_C_COMPILER_LAUNCHER:-} 22CMAKE_CXX_COMPILER_LAUNCHER=${CMAKE_CXX_COMPILER_LAUNCHER:-} 23CMAKE_MAKE_PROGRAM=${CMAKE_MAKE_PROGRAM:-} 24SKIP_TEST="${SKIP_TEST:-0}" 25BUILD_TARGET="${BUILD_TARGET:-}" 26ENABLE_WASM_SIMD="${ENABLE_WASM_SIMD:-0}" 27if [[ -n "${BUILD_TARGET}" ]]; then 28 BUILD_DIR="${BUILD_DIR:-${MYDIR}/build-${BUILD_TARGET%%-*}}" 29else 30 BUILD_DIR="${BUILD_DIR:-${MYDIR}/build}" 31fi 32# Whether we should post a message in the MR when the build fails. 33POST_MESSAGE_ON_ERROR="${POST_MESSAGE_ON_ERROR:-1}" 34 35# Set default compilers to clang if not already set 36export CC=${CC:-clang} 37export CXX=${CXX:-clang++} 38 39# Time limit for the "fuzz" command in seconds (0 means no limit). 40FUZZER_MAX_TIME="${FUZZER_MAX_TIME:-0}" 41 42SANITIZER="none" 43 44if [[ "${BUILD_TARGET}" == wasm* ]]; then 45 # Check that environment is setup for the WASM build target. 46 if [[ -z "${EMSCRIPTEN}" ]]; then 47 echo "'EMSCRIPTEN' is not defined. Use 'emconfigure' wrapper to setup WASM build environment" >&2 48 return 1 49 fi 50 # Remove the side-effect of "emconfigure" wrapper - it considers NodeJS environment. 51 unset EMMAKEN_JUST_CONFIGURE 52 EMS_TOOLCHAIN_FILE="${EMSCRIPTEN}/cmake/Modules/Platform/Emscripten.cmake" 53 if [[ -f "${EMS_TOOLCHAIN_FILE}" ]]; then 54 CMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE:-${EMS_TOOLCHAIN_FILE}} 55 else 56 echo "Warning: EMSCRIPTEN CMake module not found" >&2 57 fi 58 CMAKE_CROSSCOMPILING_EMULATOR="${MYDIR}/js-wasm-wrapper.sh" 59fi 60 61if [[ "${BUILD_TARGET%%-*}" == "x86_64" || 62 "${BUILD_TARGET%%-*}" == "i686" ]]; then 63 # Default to building all targets, even if compiler baseline is SSE4 64 HWY_BASELINE_TARGETS=${HWY_BASELINE_TARGETS:-HWY_SCALAR} 65else 66 HWY_BASELINE_TARGETS=${HWY_BASELINE_TARGETS:-} 67fi 68 69# Convenience flag to pass both CMAKE_C_FLAGS and CMAKE_CXX_FLAGS 70CMAKE_FLAGS=${CMAKE_FLAGS:-} 71CMAKE_C_FLAGS="${CMAKE_C_FLAGS:-} ${CMAKE_FLAGS}" 72CMAKE_CXX_FLAGS="${CMAKE_CXX_FLAGS:-} ${CMAKE_FLAGS}" 73 74CMAKE_CROSSCOMPILING_EMULATOR=${CMAKE_CROSSCOMPILING_EMULATOR:-} 75CMAKE_EXE_LINKER_FLAGS=${CMAKE_EXE_LINKER_FLAGS:-} 76CMAKE_FIND_ROOT_PATH=${CMAKE_FIND_ROOT_PATH:-} 77CMAKE_MODULE_LINKER_FLAGS=${CMAKE_MODULE_LINKER_FLAGS:-} 78CMAKE_SHARED_LINKER_FLAGS=${CMAKE_SHARED_LINKER_FLAGS:-} 79CMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE:-} 80 81if [[ "${ENABLE_WASM_SIMD}" -ne "0" ]]; then 82 CMAKE_CXX_FLAGS="${CMAKE_CXX_FLAGS} -msimd128" 83 CMAKE_C_FLAGS="${CMAKE_C_FLAGS} -msimd128" 84 CMAKE_EXE_LINKER_FLAGS="${CMAKE_EXE_LINKER_FLAGS} -msimd128" 85fi 86 87if [[ ! -z "${HWY_BASELINE_TARGETS}" ]]; then 88 CMAKE_CXX_FLAGS="${CMAKE_CXX_FLAGS} -DHWY_BASELINE_TARGETS=${HWY_BASELINE_TARGETS}" 89fi 90 91# Version inferred from the CI variables. 92CI_COMMIT_SHA=${CI_COMMIT_SHA:-${GITHUB_SHA:-}} 93JPEGXL_VERSION=${JPEGXL_VERSION:-${CI_COMMIT_SHA:0:8}} 94 95# Benchmark parameters 96STORE_IMAGES=${STORE_IMAGES:-1} 97BENCHMARK_CORPORA="${MYDIR}/third_party/corpora" 98 99# Local flags passed to sanitizers. 100UBSAN_FLAGS=( 101 -fsanitize=alignment 102 -fsanitize=bool 103 -fsanitize=bounds 104 -fsanitize=builtin 105 -fsanitize=enum 106 -fsanitize=float-cast-overflow 107 -fsanitize=float-divide-by-zero 108 -fsanitize=integer-divide-by-zero 109 -fsanitize=null 110 -fsanitize=object-size 111 -fsanitize=pointer-overflow 112 -fsanitize=return 113 -fsanitize=returns-nonnull-attribute 114 -fsanitize=shift-base 115 -fsanitize=shift-exponent 116 -fsanitize=unreachable 117 -fsanitize=vla-bound 118 119 -fno-sanitize-recover=undefined 120 # Brunsli uses unaligned accesses to uint32_t, so alignment is just a warning. 121 -fsanitize-recover=alignment 122) 123# -fsanitize=function doesn't work on aarch64 and arm. 124if [[ "${BUILD_TARGET%%-*}" != "aarch64" && 125 "${BUILD_TARGET%%-*}" != "arm" ]]; then 126 UBSAN_FLAGS+=( 127 -fsanitize=function 128 ) 129fi 130if [[ "${BUILD_TARGET%%-*}" != "arm" ]]; then 131 UBSAN_FLAGS+=( 132 -fsanitize=signed-integer-overflow 133 ) 134fi 135 136CLANG_TIDY_BIN=$(which clang-tidy-6.0 clang-tidy-7 clang-tidy-8 clang-tidy | head -n 1) 137# Default to "cat" if "colordiff" is not installed or if stdout is not a tty. 138if [[ -t 1 ]]; then 139 COLORDIFF_BIN=$(which colordiff cat | head -n 1) 140else 141 COLORDIFF_BIN="cat" 142fi 143FIND_BIN=$(which gfind find | head -n 1) 144# "false" will disable wine64 when not installed. This won't allow 145# cross-compiling. 146WINE_BIN=$(which wine64 false | head -n 1) 147 148CLANG_VERSION="${CLANG_VERSION:-}" 149# Detect the clang version suffix and store it in CLANG_VERSION. For example, 150# "6.0" for clang 6 or "7" for clang 7. 151detect_clang_version() { 152 if [[ -n "${CLANG_VERSION}" ]]; then 153 return 0 154 fi 155 local clang_version=$("${CC:-clang}" --version | head -n1) 156 clang_version=${clang_version#"Debian "} 157 local llvm_tag 158 case "${clang_version}" in 159 "clang version 6."*) 160 CLANG_VERSION="6.0" 161 ;; 162 "clang version "*) 163 # Any other clang version uses just the major version number. 164 local suffix="${clang_version#clang version }" 165 CLANG_VERSION="${suffix%%.*}" 166 ;; 167 "emcc"*) 168 # We can't use asan or msan in the emcc case. 169 ;; 170 *) 171 echo "Unknown clang version: ${clang_version}" >&2 172 return 1 173 esac 174} 175 176# Temporary files cleanup hooks. 177CLEANUP_FILES=() 178cleanup() { 179 if [[ ${#CLEANUP_FILES[@]} -ne 0 ]]; then 180 rm -fr "${CLEANUP_FILES[@]}" 181 fi 182} 183 184# Executed on exit. 185on_exit() { 186 local retcode="$1" 187 # Always cleanup the CLEANUP_FILES. 188 cleanup 189 190 # Post a message in the MR when requested with POST_MESSAGE_ON_ERROR but only 191 # if the run failed and we are not running from a MR pipeline. 192 if [[ ${retcode} -ne 0 && -n "${CI_BUILD_NAME:-}" && 193 -n "${POST_MESSAGE_ON_ERROR}" && -z "${CI_MERGE_REQUEST_ID:-}" && 194 "${CI_BUILD_REF_NAME}" = "master" ]]; then 195 load_mr_vars_from_commit 196 { set +xeu; } 2>/dev/null 197 local message="**Run ${CI_BUILD_NAME} @ ${CI_COMMIT_SHORT_SHA} failed.** 198 199Check the output of the job at ${CI_JOB_URL:-} to see if this was your problem. 200If it was, please rollback this change or fix the problem ASAP, broken builds 201slow down development. Check if the error already existed in the previous build 202as well. 203 204Pipeline: ${CI_PIPELINE_URL} 205 206Previous build commit: ${CI_COMMIT_BEFORE_SHA} 207" 208 cmd_post_mr_comment "${message}" 209 fi 210} 211 212trap 'retcode=$?; { set +x; } 2>/dev/null; on_exit ${retcode}' INT TERM EXIT 213 214 215# These variables are populated when calling merge_request_commits(). 216 217# The current hash at the top of the current branch or merge request branch (if 218# running from a merge request pipeline). 219MR_HEAD_SHA="" 220# The common ancestor between the current commit and the tracked branch, such 221# as master. This includes a list 222MR_ANCESTOR_SHA="" 223 224# Populate MR_HEAD_SHA and MR_ANCESTOR_SHA. 225merge_request_commits() { 226 { set +x; } 2>/dev/null 227 # GITHUB_SHA is the current reference being build in GitHub Actions. 228 if [[ -n "${GITHUB_SHA:-}" ]]; then 229 # GitHub normally does a checkout of a merge commit on a shallow repository 230 # by default. We want to get a bit more of the history to be able to diff 231 # changes on the Pull Request if needed. This fetches 10 more commits which 232 # should be enough given that PR normally should have 1 commit. 233 git -C "${MYDIR}" fetch -q origin "${GITHUB_SHA}" --depth 10 234 MR_HEAD_SHA="$(git rev-parse "FETCH_HEAD^2" 2>/dev/null || 235 echo "${GITHUB_SHA}")" 236 else 237 # CI_BUILD_REF is the reference currently being build in the CI workflow. 238 MR_HEAD_SHA=$(git -C "${MYDIR}" rev-parse -q "${CI_BUILD_REF:-HEAD}") 239 fi 240 241 if [[ -n "${CI_MERGE_REQUEST_IID:-}" ]]; then 242 # Merge request pipeline in CI. In this case the upstream is called "origin" 243 # but it refers to the forked project that's the source of the merge 244 # request. We need to get the target of the merge request, for which we need 245 # to query that repository using our CI_JOB_TOKEN. 246 echo "machine gitlab.com login gitlab-ci-token password ${CI_JOB_TOKEN}" \ 247 >> "${HOME}/.netrc" 248 git -C "${MYDIR}" fetch "${CI_MERGE_REQUEST_PROJECT_URL}" \ 249 "${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}" 250 MR_ANCESTOR_SHA=$(git -C "${MYDIR}" rev-parse -q FETCH_HEAD) 251 elif [[ -n "${GITHUB_BASE_REF:-}" ]]; then 252 # Pull request workflow in GitHub Actions. GitHub checkout action uses 253 # "origin" as the remote for the git checkout. 254 git -C "${MYDIR}" fetch -q origin "${GITHUB_BASE_REF}" 255 MR_ANCESTOR_SHA=$(git -C "${MYDIR}" rev-parse -q FETCH_HEAD) 256 else 257 # We are in a local branch, not a merge request. 258 MR_ANCESTOR_SHA=$(git -C "${MYDIR}" rev-parse -q HEAD@{upstream} || true) 259 fi 260 261 if [[ -z "${MR_ANCESTOR_SHA}" ]]; then 262 echo "Warning, not tracking any branch, using the last commit in HEAD.">&2 263 # This prints the return value with just HEAD. 264 MR_ANCESTOR_SHA=$(git -C "${MYDIR}" rev-parse -q "${MR_HEAD_SHA}^") 265 else 266 # GitHub runs the pipeline on a merge commit, no need to look for the common 267 # ancestor in that case. 268 if [[ -z "${GITHUB_BASE_REF:-}" ]]; then 269 MR_ANCESTOR_SHA=$(git -C "${MYDIR}" merge-base \ 270 "${MR_ANCESTOR_SHA}" "${MR_HEAD_SHA}") 271 fi 272 fi 273 set -x 274} 275 276# Load the MR iid from the landed commit message when running not from a 277# merge request workflow. This is useful to post back results at the merge 278# request when running pipelines from master. 279load_mr_vars_from_commit() { 280 { set +x; } 2>/dev/null 281 if [[ -z "${CI_MERGE_REQUEST_IID:-}" ]]; then 282 local mr_iid=$(git rev-list --format=%B --max-count=1 HEAD | 283 grep -F "${CI_PROJECT_URL}" | grep -F "/merge_requests" | head -n 1) 284 # mr_iid contains a string like this if it matched: 285 # Part-of: <https://gitlab.com/wg1/jpeg-xlm/merge_requests/123456> 286 if [[ -n "${mr_iid}" ]]; then 287 mr_iid=$(echo "${mr_iid}" | 288 sed -E 's,^.*merge_requests/([0-9]+)>.*$,\1,') 289 CI_MERGE_REQUEST_IID="${mr_iid}" 290 CI_MERGE_REQUEST_PROJECT_ID=${CI_PROJECT_ID} 291 fi 292 fi 293 set -x 294} 295 296# Posts a comment to the current merge request. 297cmd_post_mr_comment() { 298 { set +x; } 2>/dev/null 299 local comment="$1" 300 if [[ -n "${BOT_TOKEN:-}" && -n "${CI_MERGE_REQUEST_IID:-}" ]]; then 301 local url="${CI_API_V4_URL}/projects/${CI_MERGE_REQUEST_PROJECT_ID}/merge_requests/${CI_MERGE_REQUEST_IID}/notes" 302 curl -X POST -g \ 303 -H "PRIVATE-TOKEN: ${BOT_TOKEN}" \ 304 --data-urlencode "body=${comment}" \ 305 --output /dev/null \ 306 "${url}" 307 fi 308 set -x 309} 310 311# Set up and export the environment variables needed by the child processes. 312export_env() { 313 if [[ "${BUILD_TARGET}" == *mingw32 ]]; then 314 # Wine needs to know the paths to the mingw dlls. These should be 315 # separated by ';'. 316 WINEPATH=$("${CC:-clang}" -print-search-dirs --target="${BUILD_TARGET}" \ 317 | grep -F 'libraries: =' | cut -f 2- -d '=' | tr ':' ';') 318 # We also need our own libraries in the wine path. 319 local real_build_dir=$(realpath "${BUILD_DIR}") 320 # Some library .dll dependencies are installed in /bin: 321 export WINEPATH="${WINEPATH};${real_build_dir};${real_build_dir}/third_party/brotli;/usr/${BUILD_TARGET}/bin" 322 323 local prefix="${BUILD_DIR}/wineprefix" 324 mkdir -p "${prefix}" 325 export WINEPREFIX=$(realpath "${prefix}") 326 fi 327 # Sanitizers need these variables to print and properly format the stack 328 # traces: 329 LLVM_SYMBOLIZER=$("${CC:-clang}" -print-prog-name=llvm-symbolizer || true) 330 if [[ -n "${LLVM_SYMBOLIZER}" ]]; then 331 export ASAN_SYMBOLIZER_PATH="${LLVM_SYMBOLIZER}" 332 export MSAN_SYMBOLIZER_PATH="${LLVM_SYMBOLIZER}" 333 export UBSAN_SYMBOLIZER_PATH="${LLVM_SYMBOLIZER}" 334 fi 335} 336 337cmake_configure() { 338 export_env 339 340 if [[ "${STACK_SIZE:-0}" == 1 ]]; then 341 # Dump the stack size of each function in the .stack_sizes section for 342 # analysis. 343 CMAKE_C_FLAGS+=" -fstack-size-section" 344 CMAKE_CXX_FLAGS+=" -fstack-size-section" 345 fi 346 347 local args=( 348 -B"${BUILD_DIR}" -H"${MYDIR}" 349 -DCMAKE_BUILD_TYPE="${CMAKE_BUILD_TYPE}" 350 -G Ninja 351 -DCMAKE_CXX_FLAGS="${CMAKE_CXX_FLAGS}" 352 -DCMAKE_C_FLAGS="${CMAKE_C_FLAGS}" 353 -DCMAKE_TOOLCHAIN_FILE="${CMAKE_TOOLCHAIN_FILE}" 354 -DCMAKE_EXE_LINKER_FLAGS="${CMAKE_EXE_LINKER_FLAGS}" 355 -DCMAKE_MODULE_LINKER_FLAGS="${CMAKE_MODULE_LINKER_FLAGS}" 356 -DCMAKE_SHARED_LINKER_FLAGS="${CMAKE_SHARED_LINKER_FLAGS}" 357 -DJPEGXL_VERSION="${JPEGXL_VERSION}" 358 -DSANITIZER="${SANITIZER}" 359 # These are not enabled by default in cmake. 360 -DJPEGXL_ENABLE_VIEWERS=ON 361 -DJPEGXL_ENABLE_PLUGINS=ON 362 -DJPEGXL_ENABLE_DEVTOOLS=ON 363 # We always use libfuzzer in the ci.sh wrapper. 364 -DJPEGXL_FUZZER_LINK_FLAGS="-fsanitize=fuzzer" 365 ) 366 if [[ "${BUILD_TARGET}" != *mingw32 ]]; then 367 args+=( 368 -DJPEGXL_WARNINGS_AS_ERRORS=ON 369 ) 370 fi 371 if [[ -n "${BUILD_TARGET}" ]]; then 372 local system_name="Linux" 373 if [[ "${BUILD_TARGET}" == *mingw32 ]]; then 374 # When cross-compiling with mingw the target must be set to Windows and 375 # run programs with wine. 376 system_name="Windows" 377 args+=( 378 -DCMAKE_CROSSCOMPILING_EMULATOR="${WINE_BIN}" 379 # Normally CMake automatically defines MINGW=1 when building with the 380 # mingw compiler (x86_64-w64-mingw32-gcc) but we are normally compiling 381 # with clang. 382 -DMINGW=1 383 ) 384 fi 385 # EMSCRIPTEN toolchain sets the right values itself 386 if [[ "${BUILD_TARGET}" != wasm* ]]; then 387 # If set, BUILD_TARGET must be the target triplet such as 388 # x86_64-unknown-linux-gnu. 389 args+=( 390 -DCMAKE_C_COMPILER_TARGET="${BUILD_TARGET}" 391 -DCMAKE_CXX_COMPILER_TARGET="${BUILD_TARGET}" 392 # Only the first element of the target triplet. 393 -DCMAKE_SYSTEM_PROCESSOR="${BUILD_TARGET%%-*}" 394 -DCMAKE_SYSTEM_NAME="${system_name}" 395 ) 396 else 397 # sjpeg confuses WASM SIMD with SSE. 398 args+=( 399 -DSJPEG_ENABLE_SIMD=OFF 400 ) 401 fi 402 args+=( 403 # These are needed to make googletest work when cross-compiling. 404 -DCMAKE_CROSSCOMPILING=1 405 -DHAVE_STD_REGEX=0 406 -DHAVE_POSIX_REGEX=0 407 -DHAVE_GNU_POSIX_REGEX=0 408 -DHAVE_STEADY_CLOCK=0 409 -DHAVE_THREAD_SAFETY_ATTRIBUTES=0 410 ) 411 if [[ -z "${CMAKE_FIND_ROOT_PATH}" ]]; then 412 # find_package() will look in this prefix for libraries. 413 CMAKE_FIND_ROOT_PATH="/usr/${BUILD_TARGET}" 414 fi 415 if [[ -z "${CMAKE_PREFIX_PATH}" ]]; then 416 CMAKE_PREFIX_PATH="/usr/${BUILD_TARGET}" 417 fi 418 # Use pkg-config for the target. If there's no pkg-config available for the 419 # target we can set the PKG_CONFIG_PATH to the appropriate path in most 420 # linux distributions. 421 local pkg_config=$(which "${BUILD_TARGET}-pkg-config" || true) 422 if [[ -z "${pkg_config}" ]]; then 423 pkg_config=$(which pkg-config) 424 export PKG_CONFIG_LIBDIR="/usr/${BUILD_TARGET}/lib/pkgconfig" 425 fi 426 if [[ -n "${pkg_config}" ]]; then 427 args+=(-DPKG_CONFIG_EXECUTABLE="${pkg_config}") 428 fi 429 fi 430 if [[ -n "${CMAKE_CROSSCOMPILING_EMULATOR}" ]]; then 431 args+=( 432 -DCMAKE_CROSSCOMPILING_EMULATOR="${CMAKE_CROSSCOMPILING_EMULATOR}" 433 ) 434 fi 435 if [[ -n "${CMAKE_FIND_ROOT_PATH}" ]]; then 436 args+=( 437 -DCMAKE_FIND_ROOT_PATH="${CMAKE_FIND_ROOT_PATH}" 438 ) 439 fi 440 if [[ -n "${CMAKE_PREFIX_PATH}" ]]; then 441 args+=( 442 -DCMAKE_PREFIX_PATH="${CMAKE_PREFIX_PATH}" 443 ) 444 fi 445 if [[ -n "${CMAKE_C_COMPILER_LAUNCHER}" ]]; then 446 args+=( 447 -DCMAKE_C_COMPILER_LAUNCHER="${CMAKE_C_COMPILER_LAUNCHER}" 448 ) 449 fi 450 if [[ -n "${CMAKE_CXX_COMPILER_LAUNCHER}" ]]; then 451 args+=( 452 -DCMAKE_CXX_COMPILER_LAUNCHER="${CMAKE_CXX_COMPILER_LAUNCHER}" 453 ) 454 fi 455 if [[ -n "${CMAKE_MAKE_PROGRAM}" ]]; then 456 args+=( 457 -DCMAKE_MAKE_PROGRAM="${CMAKE_MAKE_PROGRAM}" 458 ) 459 fi 460 cmake "${args[@]}" "$@" 461} 462 463cmake_build_and_test() { 464 # gtest_discover_tests() runs the test binaries to discover the list of tests 465 # at build time, which fails under qemu. 466 ASAN_OPTIONS=detect_leaks=0 cmake --build "${BUILD_DIR}" -- all doc 467 # Pack test binaries if requested. 468 if [[ "${PACK_TEST:-}" == "1" ]]; then 469 (cd "${BUILD_DIR}" 470 ${FIND_BIN} -name '*.cmake' -a '!' -path '*CMakeFiles*' 471 # gtest / gmock / gtest_main shared libs 472 ${FIND_BIN} lib/ -name 'libg*.so*' 473 ${FIND_BIN} -type d -name tests -a '!' -path '*CMakeFiles*' 474 ) | tar -C "${BUILD_DIR}" -cf "${BUILD_DIR}/tests.tar.xz" -T - \ 475 --use-compress-program="xz --threads=$(nproc --all || echo 1) -6" 476 du -h "${BUILD_DIR}/tests.tar.xz" 477 # Pack coverage data if also available. 478 touch "${BUILD_DIR}/gcno.sentinel" 479 (cd "${BUILD_DIR}"; echo gcno.sentinel; ${FIND_BIN} -name '*gcno') | \ 480 tar -C "${BUILD_DIR}" -cvf "${BUILD_DIR}/gcno.tar.xz" -T - \ 481 --use-compress-program="xz --threads=$(nproc --all || echo 1) -6" 482 fi 483 484 if [[ "${SKIP_TEST}" -ne "1" ]]; then 485 (cd "${BUILD_DIR}" 486 export UBSAN_OPTIONS=print_stacktrace=1 487 [[ "${TEST_STACK_LIMIT}" == "none" ]] || ulimit -s "${TEST_STACK_LIMIT}" 488 ctest -j $(nproc --all || echo 1) --output-on-failure) 489 fi 490} 491 492# Configure the build to strip unused functions. This considerably reduces the 493# output size, specially for tests which only use a small part of the whole 494# library. 495strip_dead_code() { 496 # Emscripten does tree shaking without any extra flags. 497 if [[ "${CMAKE_TOOLCHAIN_FILE##*/}" == "Emscripten.cmake" ]]; then 498 return 0 499 fi 500 # -ffunction-sections, -fdata-sections and -Wl,--gc-sections effectively 501 # discard all unreachable code, reducing the code size. For this to work, we 502 # need to also pass --no-export-dynamic to prevent it from exporting all the 503 # internal symbols (like functions) making them all reachable and thus not a 504 # candidate for removal. 505 CMAKE_CXX_FLAGS+=" -ffunction-sections -fdata-sections" 506 CMAKE_C_FLAGS+=" -ffunction-sections -fdata-sections" 507 if [[ "${OS}" == "Darwin" ]]; then 508 CMAKE_EXE_LINKER_FLAGS+=" -dead_strip" 509 CMAKE_SHARED_LINKER_FLAGS+=" -dead_strip" 510 else 511 CMAKE_EXE_LINKER_FLAGS+=" -Wl,--gc-sections -Wl,--no-export-dynamic" 512 CMAKE_SHARED_LINKER_FLAGS+=" -Wl,--gc-sections -Wl,--no-export-dynamic" 513 fi 514} 515 516### Externally visible commands 517 518cmd_debug() { 519 CMAKE_BUILD_TYPE="Debug" 520 cmake_configure "$@" 521 cmake_build_and_test 522} 523 524cmd_release() { 525 CMAKE_BUILD_TYPE="Release" 526 strip_dead_code 527 cmake_configure "$@" 528 cmake_build_and_test 529} 530 531cmd_opt() { 532 CMAKE_BUILD_TYPE="RelWithDebInfo" 533 CMAKE_CXX_FLAGS+=" -DJXL_DEBUG_WARNING -DJXL_DEBUG_ON_ERROR" 534 cmake_configure "$@" 535 cmake_build_and_test 536} 537 538cmd_coverage() { 539 # -O0 prohibits stack space reuse -> causes stack-overflow on dozens of tests. 540 TEST_STACK_LIMIT="none" 541 542 cmd_release -DJPEGXL_ENABLE_COVERAGE=ON "$@" 543 544 if [[ "${SKIP_TEST}" -ne "1" ]]; then 545 # If we didn't run the test we also don't print a coverage report. 546 cmd_coverage_report 547 fi 548} 549 550cmd_coverage_report() { 551 LLVM_COV=$("${CC:-clang}" -print-prog-name=llvm-cov) 552 local real_build_dir=$(realpath "${BUILD_DIR}") 553 local gcovr_args=( 554 -r "${real_build_dir}" 555 --gcov-executable "${LLVM_COV} gcov" 556 # Only print coverage information for the libjxl directories. The rest 557 # is not part of the code under test. 558 --filter '.*jxl/.*' 559 --exclude '.*_test.cc' 560 --exclude '.*_testonly..*' 561 --exclude '.*_debug.*' 562 --exclude '.*test_utils..*' 563 --object-directory "${real_build_dir}" 564 ) 565 566 ( 567 cd "${real_build_dir}" 568 gcovr "${gcovr_args[@]}" --html --html-details \ 569 --output="${real_build_dir}/coverage.html" 570 gcovr "${gcovr_args[@]}" --print-summary | 571 tee "${real_build_dir}/coverage.txt" 572 gcovr "${gcovr_args[@]}" --xml --output="${real_build_dir}/coverage.xml" 573 ) 574} 575 576cmd_test() { 577 export_env 578 # Unpack tests if needed. 579 if [[ -e "${BUILD_DIR}/tests.tar.xz" && ! -d "${BUILD_DIR}/tests" ]]; then 580 tar -C "${BUILD_DIR}" -Jxvf "${BUILD_DIR}/tests.tar.xz" 581 fi 582 if [[ -e "${BUILD_DIR}/gcno.tar.xz" && ! -d "${BUILD_DIR}/gcno.sentinel" ]]; then 583 tar -C "${BUILD_DIR}" -Jxvf "${BUILD_DIR}/gcno.tar.xz" 584 fi 585 (cd "${BUILD_DIR}" 586 export UBSAN_OPTIONS=print_stacktrace=1 587 [[ "${TEST_STACK_LIMIT}" == "none" ]] || ulimit -s "${TEST_STACK_LIMIT}" 588 ctest -j $(nproc --all || echo 1) --output-on-failure "$@") 589} 590 591cmd_gbench() { 592 export_env 593 (cd "${BUILD_DIR}" 594 export UBSAN_OPTIONS=print_stacktrace=1 595 lib/jxl_gbench \ 596 --benchmark_counters_tabular=true \ 597 --benchmark_out_format=json \ 598 --benchmark_out=gbench.json "$@" 599 ) 600} 601 602cmd_asanfuzz() { 603 CMAKE_CXX_FLAGS+=" -fsanitize=fuzzer-no-link -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1" 604 CMAKE_C_FLAGS+=" -fsanitize=fuzzer-no-link -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1" 605 cmd_asan -DJPEGXL_ENABLE_FUZZERS=ON "$@" 606} 607 608cmd_msanfuzz() { 609 # Install msan if needed before changing the flags. 610 detect_clang_version 611 local msan_prefix="${HOME}/.msan/${CLANG_VERSION}" 612 if [[ ! -d "${msan_prefix}" || -e "${msan_prefix}/lib/libc++abi.a" ]]; then 613 # Install msan libraries for this version if needed or if an older version 614 # with libc++abi was installed. 615 cmd_msan_install 616 fi 617 618 CMAKE_CXX_FLAGS+=" -fsanitize=fuzzer-no-link -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1" 619 CMAKE_C_FLAGS+=" -fsanitize=fuzzer-no-link -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1" 620 cmd_msan -DJPEGXL_ENABLE_FUZZERS=ON "$@" 621} 622 623cmd_asan() { 624 SANITIZER="asan" 625 CMAKE_C_FLAGS+=" -DJXL_ENABLE_ASSERT=1 -g -DADDRESS_SANITIZER \ 626 -fsanitize=address ${UBSAN_FLAGS[@]}" 627 CMAKE_CXX_FLAGS+=" -DJXL_ENABLE_ASSERT=1 -g -DADDRESS_SANITIZER \ 628 -fsanitize=address ${UBSAN_FLAGS[@]}" 629 strip_dead_code 630 cmake_configure "$@" -DJPEGXL_ENABLE_TCMALLOC=OFF 631 cmake_build_and_test 632} 633 634cmd_tsan() { 635 SANITIZER="tsan" 636 local tsan_args=( 637 -DJXL_ENABLE_ASSERT=1 638 -g 639 -DTHREAD_SANITIZER 640 ${UBSAN_FLAGS[@]} 641 -fsanitize=thread 642 ) 643 CMAKE_C_FLAGS+=" ${tsan_args[@]}" 644 CMAKE_CXX_FLAGS+=" ${tsan_args[@]}" 645 646 CMAKE_BUILD_TYPE="RelWithDebInfo" 647 cmake_configure "$@" -DJPEGXL_ENABLE_TCMALLOC=OFF 648 cmake_build_and_test 649} 650 651cmd_msan() { 652 SANITIZER="msan" 653 detect_clang_version 654 local msan_prefix="${HOME}/.msan/${CLANG_VERSION}" 655 if [[ ! -d "${msan_prefix}" || -e "${msan_prefix}/lib/libc++abi.a" ]]; then 656 # Install msan libraries for this version if needed or if an older version 657 # with libc++abi was installed. 658 cmd_msan_install 659 fi 660 661 local msan_c_flags=( 662 -fsanitize=memory 663 -fno-omit-frame-pointer 664 -fsanitize-memory-track-origins 665 666 -DJXL_ENABLE_ASSERT=1 667 -g 668 -DMEMORY_SANITIZER 669 670 # Force gtest to not use the cxxbai. 671 -DGTEST_HAS_CXXABI_H_=0 672 ) 673 local msan_cxx_flags=( 674 "${msan_c_flags[@]}" 675 676 # Some C++ sources don't use the std at all, so the -stdlib=libc++ is unused 677 # in those cases. Ignore the warning. 678 -Wno-unused-command-line-argument 679 -stdlib=libc++ 680 681 # We include the libc++ from the msan directory instead, so we don't want 682 # the std includes. 683 -nostdinc++ 684 -cxx-isystem"${msan_prefix}/include/c++/v1" 685 ) 686 687 local msan_linker_flags=( 688 -L"${msan_prefix}"/lib 689 -Wl,-rpath -Wl,"${msan_prefix}"/lib/ 690 ) 691 692 CMAKE_C_FLAGS+=" ${msan_c_flags[@]} ${UBSAN_FLAGS[@]}" 693 CMAKE_CXX_FLAGS+=" ${msan_cxx_flags[@]} ${UBSAN_FLAGS[@]}" 694 CMAKE_EXE_LINKER_FLAGS+=" ${msan_linker_flags[@]}" 695 CMAKE_MODULE_LINKER_FLAGS+=" ${msan_linker_flags[@]}" 696 CMAKE_SHARED_LINKER_FLAGS+=" ${msan_linker_flags[@]}" 697 strip_dead_code 698 cmake_configure "$@" \ 699 -DCMAKE_CROSSCOMPILING=1 -DRUN_HAVE_STD_REGEX=0 -DRUN_HAVE_POSIX_REGEX=0 \ 700 -DJPEGXL_ENABLE_TCMALLOC=OFF -DJPEGXL_WARNINGS_AS_ERRORS=OFF \ 701 -DCMAKE_REQUIRED_LINK_OPTIONS="${msan_linker_flags[@]}" 702 cmake_build_and_test 703} 704 705# Install libc++ libraries compiled with msan in the msan_prefix for the current 706# compiler version. 707cmd_msan_install() { 708 local tmpdir=$(mktemp -d) 709 CLEANUP_FILES+=("${tmpdir}") 710 # Detect the llvm to install: 711 export CC="${CC:-clang}" 712 export CXX="${CXX:-clang++}" 713 detect_clang_version 714 local llvm_tag="llvmorg-${CLANG_VERSION}.0.0" 715 case "${CLANG_VERSION}" in 716 "6.0") 717 llvm_tag="llvmorg-6.0.1" 718 ;; 719 "7") 720 llvm_tag="llvmorg-7.0.1" 721 ;; 722 esac 723 local llvm_targz="${tmpdir}/${llvm_tag}.tar.gz" 724 curl -L --show-error -o "${llvm_targz}" \ 725 "https://github.com/llvm/llvm-project/archive/${llvm_tag}.tar.gz" 726 tar -C "${tmpdir}" -zxf "${llvm_targz}" 727 local llvm_root="${tmpdir}/llvm-project-${llvm_tag}" 728 729 local msan_prefix="${HOME}/.msan/${CLANG_VERSION}" 730 rm -rf "${msan_prefix}" 731 732 declare -A CMAKE_EXTRAS 733 CMAKE_EXTRAS[libcxx]="\ 734 -DLIBCXX_CXX_ABI=libstdc++ \ 735 -DLIBCXX_INSTALL_EXPERIMENTAL_LIBRARY=ON" 736 737 for project in libcxx; do 738 local proj_build="${tmpdir}/build-${project}" 739 local proj_dir="${llvm_root}/${project}" 740 mkdir -p "${proj_build}" 741 cmake -B"${proj_build}" -H"${proj_dir}" \ 742 -G Ninja \ 743 -DCMAKE_BUILD_TYPE=Release \ 744 -DLLVM_USE_SANITIZER=Memory \ 745 -DLLVM_PATH="${llvm_root}/llvm" \ 746 -DLLVM_CONFIG_PATH="$(which llvm-config llvm-config-7 llvm-config-6.0 | \ 747 head -n1)" \ 748 -DCMAKE_CXX_FLAGS="${CMAKE_CXX_FLAGS}" \ 749 -DCMAKE_C_FLAGS="${CMAKE_C_FLAGS}" \ 750 -DCMAKE_EXE_LINKER_FLAGS="${CMAKE_EXE_LINKER_FLAGS}" \ 751 -DCMAKE_SHARED_LINKER_FLAGS="${CMAKE_SHARED_LINKER_FLAGS}" \ 752 -DCMAKE_INSTALL_PREFIX="${msan_prefix}" \ 753 ${CMAKE_EXTRAS[${project}]} 754 cmake --build "${proj_build}" 755 ninja -C "${proj_build}" install 756 done 757} 758 759# Internal build step shared between all cmd_ossfuzz_* commands. 760_cmd_ossfuzz() { 761 local sanitizer="$1" 762 shift 763 mkdir -p "${BUILD_DIR}" 764 local real_build_dir=$(realpath "${BUILD_DIR}") 765 766 # oss-fuzz defines three directories: 767 # * /work, with the working directory to do re-builds 768 # * /src, with the source code to build 769 # * /out, with the output directory where to copy over the built files. 770 # We use $BUILD_DIR as the /work and the script directory as the /src. The 771 # /out directory is ignored as developers are used to look for the fuzzers in 772 # $BUILD_DIR/tools/ directly. 773 774 if [[ "${sanitizer}" = "memory" && ! -d "${BUILD_DIR}/msan" ]]; then 775 sudo docker run --rm -i \ 776 --user $(id -u):$(id -g) \ 777 -v "${real_build_dir}":/work \ 778 gcr.io/oss-fuzz-base/msan-libs-builder \ 779 bash -c "cp -r /msan /work" 780 fi 781 782 # Args passed to ninja. These will be evaluated as a string separated by 783 # spaces. 784 local jpegxl_extra_args="$@" 785 786 sudo docker run --rm -i \ 787 -e JPEGXL_UID=$(id -u) \ 788 -e JPEGXL_GID=$(id -g) \ 789 -e FUZZING_ENGINE="${FUZZING_ENGINE:-libfuzzer}" \ 790 -e SANITIZER="${sanitizer}" \ 791 -e ARCHITECTURE=x86_64 \ 792 -e FUZZING_LANGUAGE=c++ \ 793 -e MSAN_LIBS_PATH="/work/msan" \ 794 -e JPEGXL_EXTRA_ARGS="${jpegxl_extra_args}" \ 795 -v "${MYDIR}":/src/libjxl \ 796 -v "${MYDIR}/tools/ossfuzz-build.sh":/src/build.sh \ 797 -v "${real_build_dir}":/work \ 798 gcr.io/oss-fuzz/libjxl 799} 800 801cmd_ossfuzz_asan() { 802 _cmd_ossfuzz address "$@" 803} 804cmd_ossfuzz_msan() { 805 _cmd_ossfuzz memory "$@" 806} 807cmd_ossfuzz_ubsan() { 808 _cmd_ossfuzz undefined "$@" 809} 810 811cmd_ossfuzz_ninja() { 812 [[ -e "${BUILD_DIR}/build.ninja" ]] 813 local real_build_dir=$(realpath "${BUILD_DIR}") 814 815 if [[ -e "${BUILD_DIR}/msan" ]]; then 816 echo "ossfuzz_ninja doesn't work with msan builds. Use ossfuzz_msan." >&2 817 exit 1 818 fi 819 820 sudo docker run --rm -i \ 821 --user $(id -u):$(id -g) \ 822 -v "${MYDIR}":/src/libjxl \ 823 -v "${real_build_dir}":/work \ 824 gcr.io/oss-fuzz/libjxl \ 825 ninja -C /work "$@" 826} 827 828cmd_fast_benchmark() { 829 local small_corpus_tar="${BENCHMARK_CORPORA}/jyrki-full.tar" 830 mkdir -p "${BENCHMARK_CORPORA}" 831 curl --show-error -o "${small_corpus_tar}" -z "${small_corpus_tar}" \ 832 "https://storage.googleapis.com/artifacts.jpegxl.appspot.com/corpora/jyrki-full.tar" 833 834 local tmpdir=$(mktemp -d) 835 CLEANUP_FILES+=("${tmpdir}") 836 tar -xf "${small_corpus_tar}" -C "${tmpdir}" 837 838 run_benchmark "${tmpdir}" 1048576 839} 840 841cmd_benchmark() { 842 local nikon_corpus_tar="${BENCHMARK_CORPORA}/nikon-subset.tar" 843 mkdir -p "${BENCHMARK_CORPORA}" 844 curl --show-error -o "${nikon_corpus_tar}" -z "${nikon_corpus_tar}" \ 845 "https://storage.googleapis.com/artifacts.jpegxl.appspot.com/corpora/nikon-subset.tar" 846 847 local tmpdir=$(mktemp -d) 848 CLEANUP_FILES+=("${tmpdir}") 849 tar -xvf "${nikon_corpus_tar}" -C "${tmpdir}" 850 851 local sem_id="jpegxl_benchmark-$$" 852 local nprocs=$(nproc --all || echo 1) 853 images=() 854 local filename 855 while IFS= read -r filename; do 856 # This removes the './' 857 filename="${filename:2}" 858 local mode 859 if [[ "${filename:0:4}" == "srgb" ]]; then 860 mode="RGB_D65_SRG_Rel_SRG" 861 elif [[ "${filename:0:5}" == "adobe" ]]; then 862 mode="RGB_D65_Ado_Rel_Ado" 863 else 864 echo "Unknown image colorspace: ${filename}" >&2 865 exit 1 866 fi 867 png_filename="${filename%.ppm}.png" 868 png_filename=$(echo "${png_filename}" | tr '/' '_') 869 sem --bg --id "${sem_id}" -j"${nprocs}" -- \ 870 "${BUILD_DIR}/tools/decode_and_encode" \ 871 "${tmpdir}/${filename}" "${mode}" "${tmpdir}/${png_filename}" 872 images+=( "${png_filename}" ) 873 done < <(cd "${tmpdir}"; ${FIND_BIN} . -name '*.ppm' -type f) 874 sem --id "${sem_id}" --wait 875 876 # We need about 10 GiB per thread on these images. 877 run_benchmark "${tmpdir}" 10485760 878} 879 880get_mem_available() { 881 if [[ "${OS}" == "Darwin" ]]; then 882 echo $(vm_stat | grep -F 'Pages free:' | awk '{print $3 * 4}') 883 else 884 echo $(grep -F MemAvailable: /proc/meminfo | awk '{print $2}') 885 fi 886} 887 888run_benchmark() { 889 local src_img_dir="$1" 890 local mem_per_thread="${2:-10485760}" 891 892 local output_dir="${BUILD_DIR}/benchmark_results" 893 mkdir -p "${output_dir}" 894 895 # The memory available at the beginning of the benchmark run in kB. The number 896 # of threads depends on the available memory, and the passed memory per 897 # thread. We also add a 2 GiB of constant memory. 898 local mem_available="$(get_mem_available)" 899 # Check that we actually have a MemAvailable value. 900 [[ -n "${mem_available}" ]] 901 local num_threads=$(( (${mem_available} - 1048576) / ${mem_per_thread} )) 902 if [[ ${num_threads} -le 0 ]]; then 903 num_threads=1 904 fi 905 906 local benchmark_args=( 907 --input "${src_img_dir}/*.png" 908 --codec=jpeg:yuv420:q85,webp:q80,jxl:fast:d1,jxl:fast:d1:downsampling=8,jxl:fast:d4,jxl:fast:d4:downsampling=8,jxl:cheetah:m,jxl:m:cheetah:P6,jxl:m:falcon:q80 909 --output_dir "${output_dir}" 910 --noprofiler --show_progress 911 --num_threads="${num_threads}" 912 ) 913 if [[ "${STORE_IMAGES}" == "1" ]]; then 914 benchmark_args+=(--save_decompressed --save_compressed) 915 fi 916 ( 917 [[ "${TEST_STACK_LIMIT}" == "none" ]] || ulimit -s "${TEST_STACK_LIMIT}" 918 "${BUILD_DIR}/tools/benchmark_xl" "${benchmark_args[@]}" | \ 919 tee "${output_dir}/results.txt" 920 921 # Check error code for benckmark_xl command. This will exit if not. 922 return ${PIPESTATUS[0]} 923 ) 924 925 if [[ -n "${CI_BUILD_NAME:-}" ]]; then 926 { set +x; } 2>/dev/null 927 local message="Results for ${CI_BUILD_NAME} @ ${CI_COMMIT_SHORT_SHA} (job ${CI_JOB_URL:-}): 928 929$(cat "${output_dir}/results.txt") 930" 931 cmd_post_mr_comment "${message}" 932 set -x 933 fi 934} 935 936# Helper function to wait for the CPU temperature to cool down on ARM. 937wait_for_temp() { 938 { set +x; } 2>/dev/null 939 local temp_limit=${1:-38000} 940 if [[ -z "${THERMAL_FILE:-}" ]]; then 941 echo "Must define the THERMAL_FILE with the thermal_zoneX/temp file" \ 942 "to read the temperature from. This is normally set in the runner." >&2 943 exit 1 944 fi 945 local org_temp=$(cat "${THERMAL_FILE}") 946 if [[ "${org_temp}" -ge "${temp_limit}" ]]; then 947 echo -n "Waiting for temp to get down from ${org_temp}... " 948 fi 949 local temp="${org_temp}" 950 local secs=0 951 while [[ "${temp}" -ge "${temp_limit}" ]]; do 952 sleep 1 953 temp=$(cat "${THERMAL_FILE}") 954 echo -n "${temp} " 955 secs=$((secs + 1)) 956 if [[ ${secs} -ge 5 ]]; then 957 break 958 fi 959 done 960 if [[ "${org_temp}" -ge "${temp_limit}" ]]; then 961 echo "Done, temp=${temp}" 962 fi 963 set -x 964} 965 966# Helper function to set the cpuset restriction of the current process. 967cmd_cpuset() { 968 [[ "${SKIP_CPUSET:-}" != "1" ]] || return 0 969 local newset="$1" 970 local mycpuset=$(cat /proc/self/cpuset) 971 mycpuset="/dev/cpuset${mycpuset}" 972 # Check that the directory exists: 973 [[ -d "${mycpuset}" ]] 974 if [[ -e "${mycpuset}/cpuset.cpus" ]]; then 975 echo "${newset}" >"${mycpuset}/cpuset.cpus" 976 else 977 echo "${newset}" >"${mycpuset}/cpus" 978 fi 979} 980 981# Return the encoding/decoding speed from the Stats output. 982_speed_from_output() { 983 local speed="$1" 984 local unit="${2:-MP/s}" 985 if [[ "${speed}" == *"${unit}"* ]]; then 986 speed="${speed%% ${unit}*}" 987 speed="${speed##* }" 988 echo "${speed}" 989 fi 990} 991 992 993# Run benchmarks on ARM for the big and little CPUs. 994cmd_arm_benchmark() { 995 # Flags used for cjxl encoder with .png inputs 996 local jxl_png_benchmarks=( 997 # Lossy options: 998 "--epf=0 --distance=1.0 --speed=cheetah" 999 "--epf=2 --distance=1.0 --speed=cheetah" 1000 "--epf=0 --distance=8.0 --speed=cheetah" 1001 "--epf=1 --distance=8.0 --speed=cheetah" 1002 "--epf=2 --distance=8.0 --speed=cheetah" 1003 "--epf=3 --distance=8.0 --speed=cheetah" 1004 "--modular -Q 90" 1005 "--modular -Q 50" 1006 # Lossless options: 1007 "--modular" 1008 "--modular -E 0 -I 0" 1009 "--modular -P 5" 1010 "--modular --responsive=1" 1011 # Near-lossless options: 1012 "--epf=0 --distance=0.3 --speed=fast" 1013 "--modular -Q 97" 1014 ) 1015 1016 # Flags used for cjxl encoder with .jpg inputs. These should do lossless 1017 # JPEG recompression (of pixels or full jpeg). 1018 local jxl_jpeg_benchmarks=( 1019 "--num_reps=3" 1020 ) 1021 1022 local images=( 1023 "third_party/testdata/imagecompression.info/flower_foveon.png" 1024 ) 1025 1026 local jpg_images=( 1027 "third_party/testdata/imagecompression.info/flower_foveon.png.im_q85_420.jpg" 1028 ) 1029 1030 if [[ "${SKIP_CPUSET:-}" == "1" ]]; then 1031 # Use a single cpu config in this case. 1032 local cpu_confs=("?") 1033 else 1034 # Otherwise the CPU config comes from the environment: 1035 local cpu_confs=( 1036 "${RUNNER_CPU_LITTLE}" 1037 "${RUNNER_CPU_BIG}" 1038 # The CPU description is something like 3-7, so these configurations only 1039 # take the first CPU of the group. 1040 "${RUNNER_CPU_LITTLE%%-*}" 1041 "${RUNNER_CPU_BIG%%-*}" 1042 ) 1043 # Check that RUNNER_CPU_ALL is defined. In the SKIP_CPUSET=1 case this will 1044 # be ignored but still evaluated when calling cmd_cpuset. 1045 [[ -n "${RUNNER_CPU_ALL}" ]] 1046 fi 1047 1048 local jpg_dirname="third_party/corpora/jpeg" 1049 mkdir -p "${jpg_dirname}" 1050 local jpg_qualities=( 50 80 95 ) 1051 for src_img in "${images[@]}"; do 1052 for q in "${jpg_qualities[@]}"; do 1053 local jpeg_name="${jpg_dirname}/"$(basename "${src_img}" .png)"-q${q}.jpg" 1054 convert -sampling-factor 1x1 -quality "${q}" \ 1055 "${src_img}" "${jpeg_name}" 1056 jpg_images+=("${jpeg_name}") 1057 done 1058 done 1059 1060 local output_dir="${BUILD_DIR}/benchmark_results" 1061 mkdir -p "${output_dir}" 1062 local runs_file="${output_dir}/runs.txt" 1063 1064 if [[ ! -e "${runs_file}" ]]; then 1065 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)" | 1066 tee -a "${runs_file}" 1067 fi 1068 1069 mkdir -p "${BUILD_DIR}/arm_benchmark" 1070 local flags 1071 local src_img 1072 for src_img in "${jpg_images[@]}" "${images[@]}"; do 1073 local src_img_hash=$(sha1sum "${src_img}" | cut -f 1 -d ' ') 1074 local enc_binaries=("${BUILD_DIR}/tools/cjxl") 1075 local src_ext="${src_img##*.}" 1076 for enc_binary in "${enc_binaries[@]}"; do 1077 local enc_binary_base=$(basename "${enc_binary}") 1078 1079 # Select the list of flags to use for the current encoder/image pair. 1080 local img_benchmarks 1081 if [[ "${src_ext}" == "jpg" ]]; then 1082 img_benchmarks=("${jxl_jpeg_benchmarks[@]}") 1083 else 1084 img_benchmarks=("${jxl_png_benchmarks[@]}") 1085 fi 1086 1087 for flags in "${img_benchmarks[@]}"; do 1088 # Encoding step. 1089 local enc_file_hash="${enc_binary_base} || $flags || ${src_img} || ${src_img_hash}" 1090 enc_file_hash=$(echo "${enc_file_hash}" | sha1sum | cut -f 1 -d ' ') 1091 local enc_file="${BUILD_DIR}/arm_benchmark/${enc_file_hash}.jxl" 1092 1093 for cpu_conf in "${cpu_confs[@]}"; do 1094 cmd_cpuset "${cpu_conf}" 1095 # nproc returns the number of active CPUs, which is given by the cpuset 1096 # mask. 1097 local num_threads="$(nproc)" 1098 1099 echo "Encoding with: ${enc_binary_base} img=${src_img} cpus=${cpu_conf} enc_flags=${flags}" 1100 local enc_output 1101 if [[ "${flags}" == *"modular"* ]]; then 1102 # We don't benchmark encoding speed in this case. 1103 if [[ ! -f "${enc_file}" ]]; then 1104 cmd_cpuset "${RUNNER_CPU_ALL:-}" 1105 "${enc_binary}" ${flags} "${src_img}" "${enc_file}.tmp" 1106 mv "${enc_file}.tmp" "${enc_file}" 1107 cmd_cpuset "${cpu_conf}" 1108 fi 1109 enc_output=" ?? MP/s" 1110 else 1111 wait_for_temp 1112 enc_output=$("${enc_binary}" ${flags} "${src_img}" "${enc_file}.tmp" \ 1113 2>&1 | tee /dev/stderr | grep -F "MP/s [") 1114 mv "${enc_file}.tmp" "${enc_file}" 1115 fi 1116 local enc_speed=$(_speed_from_output "${enc_output}") 1117 local enc_size=$(stat -c "%s" "${enc_file}") 1118 1119 echo "Decoding with: img=${src_img} cpus=${cpu_conf} enc_flags=${flags}" 1120 1121 local dec_output 1122 wait_for_temp 1123 dec_output=$("${BUILD_DIR}/tools/djxl" "${enc_file}" \ 1124 --num_reps=5 --num_threads="${num_threads}" 2>&1 | tee /dev/stderr | 1125 grep -E "M[BP]/s \[") 1126 local img_size=$(echo "${dec_output}" | cut -f 1 -d ',') 1127 local img_size_x=$(echo "${img_size}" | cut -f 1 -d ' ') 1128 local img_size_y=$(echo "${img_size}" | cut -f 3 -d ' ') 1129 local img_size_px=$(( ${img_size_x} * ${img_size_y} )) 1130 local dec_speed=$(_speed_from_output "${dec_output}") 1131 1132 # For JPEG lossless recompression modes (where the original is a JPEG) 1133 # decode to JPG as well. 1134 local jpeg_dec_mps_speed="" 1135 local jpeg_dec_mbs_speed="" 1136 if [[ "${src_ext}" == "jpg" ]]; then 1137 wait_for_temp 1138 local dec_file="${BUILD_DIR}/arm_benchmark/${enc_file_hash}.jpg" 1139 dec_output=$("${BUILD_DIR}/tools/djxl" "${enc_file}" \ 1140 "${dec_file}" --num_reps=5 --num_threads="${num_threads}" 2>&1 | \ 1141 tee /dev/stderr | grep -E "M[BP]/s \[") 1142 local jpeg_dec_mps_speed=$(_speed_from_output "${dec_output}") 1143 local jpeg_dec_mbs_speed=$(_speed_from_output "${dec_output}" MB/s) 1144 if ! cmp --quiet "${src_img}" "${dec_file}"; then 1145 # Add a start at the end to signal that the files are different. 1146 jpeg_dec_mbs_speed+="*" 1147 fi 1148 fi 1149 1150 # Record entry in a tab-separated file. 1151 local src_img_base=$(basename "${src_img}") 1152 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}" | 1153 tee -a "${runs_file}" 1154 done 1155 done 1156 done 1157 done 1158 cmd_cpuset "${RUNNER_CPU_ALL:-}" 1159 cat "${runs_file}" 1160 1161 if [[ -n "${CI_BUILD_NAME:-}" ]]; then 1162 load_mr_vars_from_commit 1163 { set +x; } 2>/dev/null 1164 local message="Results for ${CI_BUILD_NAME} @ ${CI_COMMIT_SHORT_SHA} (job ${CI_JOB_URL:-}): 1165 1166\`\`\` 1167$(column -t -s " " "${runs_file}") 1168\`\`\` 1169" 1170 cmd_post_mr_comment "${message}" 1171 set -x 1172 fi 1173} 1174 1175# Generate a corpus and run the fuzzer on that corpus. 1176cmd_fuzz() { 1177 local corpus_dir=$(realpath "${BUILD_DIR}/fuzzer_corpus") 1178 local fuzzer_crash_dir=$(realpath "${BUILD_DIR}/fuzzer_crash") 1179 mkdir -p "${corpus_dir}" "${fuzzer_crash_dir}" 1180 # Generate step. 1181 "${BUILD_DIR}/tools/fuzzer_corpus" "${corpus_dir}" 1182 # Run step: 1183 local nprocs=$(nproc --all || echo 1) 1184 ( 1185 cd "${BUILD_DIR}" 1186 "tools/djxl_fuzzer" "${fuzzer_crash_dir}" "${corpus_dir}" \ 1187 -max_total_time="${FUZZER_MAX_TIME}" -jobs=${nprocs} \ 1188 -artifact_prefix="${fuzzer_crash_dir}/" 1189 ) 1190} 1191 1192# Runs the linter (clang-format) on the pending CLs. 1193cmd_lint() { 1194 merge_request_commits 1195 { set +x; } 2>/dev/null 1196 local versions=(${1:-6.0 7 8 9 10 11}) 1197 local clang_format_bins=("${versions[@]/#/clang-format-}" clang-format) 1198 local tmpdir=$(mktemp -d) 1199 CLEANUP_FILES+=("${tmpdir}") 1200 1201 local ret=0 1202 local build_patch="${tmpdir}/build_cleaner.patch" 1203 if ! "${MYDIR}/tools/build_cleaner.py" >"${build_patch}"; then 1204 ret=1 1205 echo "build_cleaner.py findings:" >&2 1206 "${COLORDIFF_BIN}" <"${build_patch}" 1207 echo "Run \`tools/build_cleaner.py --update\` to apply them" >&2 1208 fi 1209 1210 local installed=() 1211 local clang_patch 1212 local clang_format 1213 for clang_format in "${clang_format_bins[@]}"; do 1214 if ! which "${clang_format}" >/dev/null; then 1215 continue 1216 fi 1217 installed+=("${clang_format}") 1218 local tmppatch="${tmpdir}/${clang_format}.patch" 1219 # We include in this linter all the changes including the uncommitted changes 1220 # to avoid printing changes already applied. 1221 set -x 1222 git -C "${MYDIR}" "${clang_format}" --binary "${clang_format}" \ 1223 --style=file --diff "${MR_ANCESTOR_SHA}" -- >"${tmppatch}" 1224 { set +x; } 2>/dev/null 1225 1226 if grep -E '^--- ' "${tmppatch}">/dev/null; then 1227 if [[ -n "${LINT_OUTPUT:-}" ]]; then 1228 cp "${tmppatch}" "${LINT_OUTPUT}" 1229 fi 1230 clang_patch="${tmppatch}" 1231 else 1232 echo "clang-format check OK" >&2 1233 return ${ret} 1234 fi 1235 done 1236 1237 if [[ ${#installed[@]} -eq 0 ]]; then 1238 echo "You must install clang-format for \"git clang-format\"" >&2 1239 exit 1 1240 fi 1241 1242 # clang-format is installed but found problems. 1243 echo "clang-format findings:" >&2 1244 "${COLORDIFF_BIN}" < "${clang_patch}" 1245 1246 echo "clang-format found issues in your patches from ${MR_ANCESTOR_SHA}" \ 1247 "to the current patch. Run \`./ci.sh lint | patch -p1\` from the base" \ 1248 "directory to apply them." >&2 1249 exit 1 1250} 1251 1252# Runs clang-tidy on the pending CLs. If the "all" argument is passed it runs 1253# clang-tidy over all the source files instead. 1254cmd_tidy() { 1255 local what="${1:-}" 1256 1257 if [[ -z "${CLANG_TIDY_BIN}" ]]; then 1258 echo "ERROR: You must install clang-tidy-7 or newer to use ci.sh tidy" >&2 1259 exit 1 1260 fi 1261 1262 local git_args=() 1263 if [[ "${what}" == "all" ]]; then 1264 git_args=(ls-files) 1265 shift 1266 else 1267 merge_request_commits 1268 git_args=( 1269 diff-tree --no-commit-id --name-only -r "${MR_ANCESTOR_SHA}" 1270 "${MR_HEAD_SHA}" 1271 ) 1272 fi 1273 1274 # Clang-tidy needs the compilation database generated by cmake. 1275 if [[ ! -e "${BUILD_DIR}/compile_commands.json" ]]; then 1276 # Generate the build options in debug mode, since we need the debug asserts 1277 # enabled for the clang-tidy analyzer to use them. 1278 CMAKE_BUILD_TYPE="Debug" 1279 cmake_configure 1280 # Build the autogen targets to generate the .h files from the .ui files. 1281 local autogen_targets=( 1282 $(ninja -C "${BUILD_DIR}" -t targets | grep -F _autogen: | 1283 cut -f 1 -d :) 1284 ) 1285 if [[ ${#autogen_targets[@]} != 0 ]]; then 1286 ninja -C "${BUILD_DIR}" "${autogen_targets[@]}" 1287 fi 1288 fi 1289 1290 cd "${MYDIR}" 1291 local nprocs=$(nproc --all || echo 1) 1292 local ret=0 1293 if ! parallel -j"${nprocs}" --keep-order -- \ 1294 "${CLANG_TIDY_BIN}" -p "${BUILD_DIR}" -format-style=file -quiet "$@" {} \ 1295 < <(git "${git_args[@]}" | grep -E '(\.cc|\.cpp)$') \ 1296 >"${BUILD_DIR}/clang-tidy.txt"; then 1297 ret=1 1298 fi 1299 { set +x; } 2>/dev/null 1300 echo "Findings statistics:" >&2 1301 grep -E ' \[[A-Za-z\.,\-]+\]' -o "${BUILD_DIR}/clang-tidy.txt" | sort \ 1302 | uniq -c >&2 1303 1304 if [[ $ret -ne 0 ]]; then 1305 cat >&2 <<EOF 1306Errors found, see ${BUILD_DIR}/clang-tidy.txt for details. 1307To automatically fix them, run: 1308 1309 SKIP_TEST=1 ./ci.sh debug 1310 ${CLANG_TIDY_BIN} -p ${BUILD_DIR} -fix -format-style=file -quiet $@ \$(git ${git_args[@]} | grep -E '(\.cc|\.cpp)\$') 1311EOF 1312 fi 1313 1314 return ${ret} 1315} 1316 1317# Print stats about all the packages built in ${BUILD_DIR}/debs/. 1318cmd_debian_stats() { 1319 { set +x; } 2>/dev/null 1320 local debsdir="${BUILD_DIR}/debs" 1321 local f 1322 while IFS='' read -r -d '' f; do 1323 echo "=====================================================================" 1324 echo "Package $f:" 1325 dpkg --info $f 1326 dpkg --contents $f 1327 done < <(find "${BUILD_DIR}/debs" -maxdepth 1 -mindepth 1 -type f \ 1328 -name '*.deb' -print0) 1329} 1330 1331build_debian_pkg() { 1332 local srcdir="$1" 1333 local srcpkg="$2" 1334 1335 local debsdir="${BUILD_DIR}/debs" 1336 local builddir="${debsdir}/${srcpkg}" 1337 1338 # debuild doesn't have an easy way to build out of tree, so we make a copy 1339 # of with all symlinks on the first level. 1340 mkdir -p "${builddir}" 1341 for f in $(find "${srcdir}" -mindepth 1 -maxdepth 1 -printf '%P\n'); do 1342 if [[ ! -L "${builddir}/$f" ]]; then 1343 rm -f "${builddir}/$f" 1344 ln -s "${srcdir}/$f" "${builddir}/$f" 1345 fi 1346 done 1347 ( 1348 cd "${builddir}" 1349 debuild -b -uc -us 1350 ) 1351} 1352 1353cmd_debian_build() { 1354 local srcpkg="${1:-}" 1355 1356 case "${srcpkg}" in 1357 jpeg-xl) 1358 build_debian_pkg "${MYDIR}" "jpeg-xl" 1359 ;; 1360 highway) 1361 build_debian_pkg "${MYDIR}/third_party/highway" "highway" 1362 ;; 1363 *) 1364 echo "ERROR: Must pass a valid source package name to build." >&2 1365 ;; 1366 esac 1367} 1368 1369get_version() { 1370 local varname=$1 1371 local line=$(grep -F "set(${varname} " lib/CMakeLists.txt | head -n 1) 1372 [[ -n "${line}" ]] 1373 line="${line#set(${varname} }" 1374 line="${line%)}" 1375 echo "${line}" 1376} 1377 1378cmd_bump_version() { 1379 local newver="${1:-}" 1380 1381 if ! which dch >/dev/null; then 1382 echo "Run:\n sudo apt install debhelper" 1383 exit 1 1384 fi 1385 1386 if [[ -z "${newver}" ]]; then 1387 local major=$(get_version JPEGXL_MAJOR_VERSION) 1388 local minor=$(get_version JPEGXL_MINOR_VERSION) 1389 local patch=0 1390 minor=$(( ${minor} + 1)) 1391 else 1392 local major="${newver%%.*}" 1393 newver="${newver#*.}" 1394 local minor="${newver%%.*}" 1395 newver="${newver#${minor}}" 1396 local patch="${newver#.}" 1397 if [[ -z "${patch}" ]]; then 1398 patch=0 1399 fi 1400 fi 1401 1402 newver="${major}.${minor}" 1403 if [[ "${patch}" != "0" ]]; then 1404 newver="${newver}.${patch}" 1405 fi 1406 echo "Bumping version to ${newver} (${major}.${minor}.${patch})" 1407 sed -E \ 1408 -e "s/(set\\(JPEGXL_MAJOR_VERSION) [0-9]+\\)/\\1 ${major})/" \ 1409 -e "s/(set\\(JPEGXL_MINOR_VERSION) [0-9]+\\)/\\1 ${minor})/" \ 1410 -e "s/(set\\(JPEGXL_PATCH_VERSION) [0-9]+\\)/\\1 ${patch})/" \ 1411 -i lib/CMakeLists.txt 1412 1413 # Update lib.gni 1414 tools/build_cleaner.py --update 1415 1416 # Mark the previous version as "unstable". 1417 DEBCHANGE_RELEASE_HEURISTIC=log dch -M --distribution unstable --release '' 1418 DEBCHANGE_RELEASE_HEURISTIC=log dch -M \ 1419 --newversion "${newver}" \ 1420 "Bump JPEG XL version to ${newver}." 1421} 1422 1423# Check that the AUTHORS file contains the email of the committer. 1424cmd_authors() { 1425 merge_request_commits 1426 # TODO(deymo): Handle multiple commits and check that they are all the same 1427 # author. 1428 local email=$(git log --format='%ae' "${MR_HEAD_SHA}^!") 1429 local name=$(git log --format='%an' "${MR_HEAD_SHA}^!") 1430 "${MYDIR}"/tools/check_author.py "${email}" "${name}" 1431} 1432 1433main() { 1434 local cmd="${1:-}" 1435 if [[ -z "${cmd}" ]]; then 1436 cat >&2 <<EOF 1437Use: $0 CMD 1438 1439Where cmd is one of: 1440 opt Build and test a Release with symbols build. 1441 debug Build and test a Debug build (NDEBUG is not defined). 1442 release Build and test a striped Release binary without debug information. 1443 asan Build and test an ASan (AddressSanitizer) build. 1444 msan Build and test an MSan (MemorySanitizer) build. Needs to have msan 1445 c++ libs installed with msan_install first. 1446 tsan Build and test a TSan (ThreadSanitizer) build. 1447 asanfuzz Build and test an ASan (AddressSanitizer) build for fuzzing. 1448 msanfuzz Build and test an MSan (MemorySanitizer) build for fuzzing. 1449 test Run the tests build by opt, debug, release, asan or msan. Useful when 1450 building with SKIP_TEST=1. 1451 gbench Run the Google benchmark tests. 1452 fuzz Generate the fuzzer corpus and run the fuzzer on it. Useful after 1453 building with asan or msan. 1454 benchmark Run the benchmark over the default corpus. 1455 fast_benchmark Run the benchmark over the small corpus. 1456 1457 coverage Buils and run tests with coverage support. Runs coverage_report as 1458 well. 1459 coverage_report Generate HTML, XML and text coverage report after a coverage 1460 run. 1461 1462 lint Run the linter checks on the current commit or merge request. 1463 tidy Run clang-tidy on the current commit or merge request. 1464 authors Check that the last commit's author is listed in the AUTHORS file. 1465 1466 msan_install Install the libc++ libraries required to build in msan mode. This 1467 needs to be done once. 1468 1469 debian_build <srcpkg> Build the given source package. 1470 debian_stats Print stats about the built packages. 1471 1472oss-fuzz commands: 1473 ossfuzz_asan Build the local source inside oss-fuzz docker with asan. 1474 ossfuzz_msan Build the local source inside oss-fuzz docker with msan. 1475 ossfuzz_ubsan Build the local source inside oss-fuzz docker with ubsan. 1476 ossfuzz_ninja Run ninja on the BUILD_DIR inside the oss-fuzz docker. Extra 1477 parameters are passed to ninja, for example "djxl_fuzzer" will 1478 only build that ninja target. Use for faster build iteration 1479 after one of the ossfuzz_*san commands. 1480 1481You can pass some optional environment variables as well: 1482 - BUILD_DIR: The output build directory (by default "$$repo/build") 1483 - BUILD_TARGET: The target triplet used when cross-compiling. 1484 - CMAKE_FLAGS: Convenience flag to pass both CMAKE_C_FLAGS and CMAKE_CXX_FLAGS. 1485 - CMAKE_PREFIX_PATH: Installation prefixes to be searched by the find_package. 1486 - ENABLE_WASM_SIMD=1: enable experimental SIMD in WASM build (only). 1487 - FUZZER_MAX_TIME: "fuzz" command fuzzer running timeout in seconds. 1488 - LINT_OUTPUT: Path to the output patch from the "lint" command. 1489 - SKIP_CPUSET=1: Skip modifying the cpuset in the arm_benchmark. 1490 - SKIP_TEST=1: Skip the test stage. 1491 - STORE_IMAGES=0: Makes the benchmark discard the computed images. 1492 - TEST_STACK_LIMIT: Stack size limit (ulimit -s) during tests, in KiB. 1493 - STACK_SIZE=1: Generate binaries with the .stack_sizes sections. 1494 1495These optional environment variables are forwarded to the cmake call as 1496parameters: 1497 - CMAKE_BUILD_TYPE 1498 - CMAKE_C_FLAGS 1499 - CMAKE_CXX_FLAGS 1500 - CMAKE_C_COMPILER_LAUNCHER 1501 - CMAKE_CXX_COMPILER_LAUNCHER 1502 - CMAKE_CROSSCOMPILING_EMULATOR 1503 - CMAKE_FIND_ROOT_PATH 1504 - CMAKE_EXE_LINKER_FLAGS 1505 - CMAKE_MAKE_PROGRAM 1506 - CMAKE_MODULE_LINKER_FLAGS 1507 - CMAKE_SHARED_LINKER_FLAGS 1508 - CMAKE_TOOLCHAIN_FILE 1509 1510Example: 1511 BUILD_DIR=/tmp/build $0 opt 1512EOF 1513 exit 1 1514 fi 1515 1516 cmd="cmd_${cmd}" 1517 shift 1518 set -x 1519 "${cmd}" "$@" 1520} 1521 1522main "$@" 1523