chromium/chrome/installer/linux/common/installer.include

# Shows the output of a given command only on failure, or when VERBOSE is set.
log_cmd() {
  if [ "${VERBOSE:-}" ]; then
    "$@"
  else
    # Record $- into a separate variable because it gets reset in the subshell.
    FORWARD_SHELL_OPTS=$-
    ERREXIT=$(echo ${FORWARD_SHELL_OPTS} | grep -o e || true)
    set +${ERREXIT}
    CMD_OUTPUT=$("$@" 2>&1)
    ERRCODE=$?
    set -${ERREXIT}
    if [ ${ERRCODE} -ne 0 ]; then
      echo "$@"
      echo "${CMD_OUTPUT}"
      if [ ${ERREXIT} ]; then
        exit ${ERRCODE}
      fi
    fi
  fi
}

# Recursively replace @@include@@ template variables with the referenced file,
# and write the resulting text to stdout.
process_template_includes() {
  INCSTACK+="$1->"
  # Includes are relative to the file that does the include.
  INCDIR=$(dirname $1)
  # Clear IFS so 'read' doesn't trim whitespace
  local OLDIFS="$IFS"
  IFS=''
  while read -r LINE
  do
    INCLINE=$(sed -e '/^[[:space:]]*@@include@@/!d' <<<$LINE)
    if [ -n "$INCLINE" ]; then
      INCFILE=$(echo $INCLINE | sed -e "s#@@include@@\(.*\)#\1#")
      # Simple filename match to detect cyclic includes.
      CYCLE=$(sed -e "\#$INCFILE#"'!d' <<<$INCSTACK)
      if [ "$CYCLE" ]; then
        echo "ERROR: Possible cyclic include detected." 1>&2
        echo "$INCSTACK$INCFILE" 1>&2
        exit 1
      fi
      if [ ! -r "$INCDIR/$INCFILE" ]; then
        echo "ERROR: Couldn't read include file: $INCDIR/$INCFILE" 1>&2
        exit 1
      fi
      process_template_includes "$INCDIR/$INCFILE"
    else
      echo "$LINE"
    fi
  done < "$1"
  IFS="$OLDIFS"
  INCSTACK=${INCSTACK%"$1->"}
}

# Replace template variables (@@VARNAME@@) in the given template file. If a
# second argument is given, save the processed text to that filename, otherwise
# modify the template file in place.
process_template() (
  # Don't worry if some of these substitution variables aren't set.
  # Note that this function is run in a sub-shell so we don't leak this
  # setting, since we still want unbound variables to be an error elsewhere.
  set +u

  local TMPLIN="$1"
  if [ -z "$2" ]; then
    local TMPLOUT="$TMPLIN"
  else
    local TMPLOUT="$2"
  fi
  # Process includes first so included text also gets substitutions.
  TMPLINCL="$(process_template_includes "$TMPLIN")"
  sed \
    -e "s#@@PACKAGE@@#${PACKAGE}#g" \
    -e "s#@@PACKAGE_ORIG@@#${PACKAGE_ORIG}#g" \
    -e "s#@@PACKAGE_FILENAME@@#${PACKAGE_FILENAME}#g" \
    -e "s#@@PROGNAME@@#${PROGNAME}#g" \
    -e "s#@@CHANNEL@@#${CHANNEL}#g" \
    -e "s#@@COMPANY_FULLNAME@@#${COMPANY_FULLNAME}#g" \
    -e "s#@@VERSION@@#${VERSION}#g" \
    -e "s#@@PACKAGE_RELEASE@@#${PACKAGE_RELEASE}#g" \
    -e "s#@@VERSIONFULL@@#${VERSIONFULL}#g" \
    -e "s#@@INSTALLDIR@@#${INSTALLDIR}#g" \
    -e "s#@@BUILDDIR@@#${OUTPUTDIR}#g" \
    -e "s#@@STAGEDIR@@#${STAGEDIR}#g" \
    -e "s#@@SCRIPTDIR@@#${SCRIPTDIR}#g" \
    -e "s#@@ENROLLMENTDIR@@#${ENROLLMENTDIR}#g" \
    -e "s#@@MENUNAME@@#${MENUNAME}#g" \
    -e "s#@@PRODUCTURL@@#${PRODUCTURL}#g" \
    -e "s#@@PREDEPENDS@@#${PREDEPENDS}#g" \
    -e "s#@@DEPENDS@@#${DEPENDS}#g" \
    -e "s#@@PROVIDES@@#${PROVIDES}#g" \
    -e "s#@@ARCHITECTURE@@#${ARCHITECTURE}#g" \
    -e "s#@@MAINTNAME@@#${MAINTNAME}#g" \
    -e "s#@@MAINTMAIL@@#${MAINTMAIL}#g" \
    -e "s#@@REPOCONFIG@@#${REPOCONFIG}#g" \
    -e "s#@@REPOCONFIGREGEX@@#${REPOCONFIGREGEX}#g" \
    -e "s#@@SHORTDESC@@#${SHORTDESC}#g" \
    -e "s#@@FULLDESC@@#${FULLDESC}#g" \
    -e "s#@@USR_BIN_SYMLINK_NAME@@#${USR_BIN_SYMLINK_NAME:-}#g" \
    -e "s#@@LOGO_RESOURCES_PNG@@#${LOGO_RESOURCES_PNG}#g" \
    -e "s#@@LOGO_RESOURCE_XPM@@#${LOGO_RESOURCE_XPM}#g" \
    -e "s#@@DATE_RFC5322@@#$(date --rfc-email)#g" \
    > "$TMPLOUT" <<< "$TMPLINCL"
)

# Setup the installation directory hierarchy in the package staging area.
prep_staging_common() {
  install -m 755 -d "${STAGEDIR}/${INSTALLDIR}" \
    "${STAGEDIR}/usr/bin" \
    "${STAGEDIR}/usr/share/applications" \
    "${STAGEDIR}/usr/share/appdata" \
    "${STAGEDIR}/usr/share/gnome-control-center/default-apps" \
    "${STAGEDIR}/usr/share/man/man1"
}

get_version_info() {
  source "${OUTPUTDIR}/installer/version.txt"
  VERSION="${MAJOR}.${MINOR}.${BUILD}.${PATCH}"
  # TODO(phajdan.jr): Provide a mechanism to pass a different package
  # release number if needed. The meaning of it is to bump it for
  # packaging-only changes while the underlying software has the same version.
  # This corresponds to the Release field in RPM spec files and debian_revision
  # component of the Version field for DEB control file.
  # Generally with Chrome's fast release cycle it'd be more hassle to try
  # to bump this number between releases.
  PACKAGE_RELEASE="1"
}

stage_install_common() {
  log_cmd echo "Staging common install files in '${STAGEDIR}'..."

  # Note: Changes here may also need to be applied to ChromeOS's
  # chromite/lib/chrome_util.py.

  # Note: This only supports static binaries and does not work when the GN
  # is_component_build flag is true.

  # app
  STRIPPEDFILE="${OUTPUTDIR}/${PROGNAME}.stripped"
  install -m 755 "${STRIPPEDFILE}" "${STAGEDIR}/${INSTALLDIR}/${PROGNAME}"

  # crashpad
  strippedfile="${OUTPUTDIR}/chrome_crashpad_handler.stripped"
  install -m 755 "${strippedfile}" "${STAGEDIR}/${INSTALLDIR}/chrome_crashpad_handler"

  # Final permissions for the chrome-management-service will be set in
  # postinst chrome_management_service_setup().
  strippedfile="${OUTPUTDIR}/chrome_management_service.stripped"
  install -m 755 "${strippedfile}" "${STAGEDIR}/${INSTALLDIR}/chrome-management-service"

  # resources
  install -m 644 "${OUTPUTDIR}/resources.pak" "${STAGEDIR}/${INSTALLDIR}/"
  # TODO(mmoss): This has broken a couple times on adding new .pak files. Maybe
  # we should flag all installer files in FILES.cfg and get them from there, so
  # there's only one place people need to keep track of such things (and in
  # only the public repository).
  if [ -r "${OUTPUTDIR}/chrome_100_percent.pak" ]; then
    install -m 644 "${OUTPUTDIR}/chrome_100_percent.pak" "${STAGEDIR}/${INSTALLDIR}/"
    install -m 644 "${OUTPUTDIR}/chrome_200_percent.pak" "${STAGEDIR}/${INSTALLDIR}/"
  else
    install -m 644 "${OUTPUTDIR}/theme_resources_100_percent.pak" "${STAGEDIR}/${INSTALLDIR}/"
    install -m 644 "${OUTPUTDIR}/ui_resources_100_percent.pak" "${STAGEDIR}/${INSTALLDIR}/"
  fi

  # ICU data file; Necessary when the GN icu_use_data_file flag is true.
  install -m 644 "${OUTPUTDIR}/icudtl.dat" "${STAGEDIR}/${INSTALLDIR}/"

  # V8 snapshot files; Necessary when the GN v8_use_external_startup_data flag
  # is true.
  # Use v8_context_snapshot.bin instead of snapshot_blob.bin if it is available.
  # TODO(crbug.com/40539769): Unship snapshot_blob.bin on ChromeOS and drop this branch
  if [ -f "${OUTPUTDIR}/v8_context_snapshot.bin" ]; then
    install -m 644 "${OUTPUTDIR}/v8_context_snapshot.bin" "${STAGEDIR}/${INSTALLDIR}/"
  else
    install -m 644 "${OUTPUTDIR}/snapshot_blob.bin" "${STAGEDIR}/${INSTALLDIR}/"
  fi

  # sandbox
  # Rename sandbox binary with hyphen instead of underscore because that's what
  # the code looks for. Originally, the SCons build system may have had a bug
  # where it did not support hyphens, so this is stuck as is to avoid breaking
  # anyone who expects the build artifact to have the underscore.
  # the code looks for, but the build targets can't use hyphens (scons bug?)
  strippedfile="${OUTPUTDIR}/${PROGNAME}_sandbox.stripped"
  install -m 4755 "${strippedfile}" "${STAGEDIR}/${INSTALLDIR}/chrome-sandbox"

  # l10n paks
  install -m 755 -d "${STAGEDIR}/${INSTALLDIR}/locales/"
  find "${OUTPUTDIR}/locales" -type f -name '*.pak' -exec \
    cp -a '{}' "${STAGEDIR}/${INSTALLDIR}/locales/" \;
  find "${STAGEDIR}/${INSTALLDIR}/locales" -type f -exec chmod 644 '{}' \;

  # TODO(crbug.com/40688962): The below conditions check for the
  # existence of files to determine if they should be copied to the staging
  # directory.  However, these may be stale if the build config no longer
  # builds these files.  The build config should be obtained from gn rather than
  # guessed based on the presence of files.

  # Privacy Sandbox Attestation
  if [ -f "${OUTPUTDIR}/PrivacySandboxAttestationsPreloaded/manifest.json" ]; then
    install -m 755 -d "${STAGEDIR}/${INSTALLDIR}/PrivacySandboxAttestationsPreloaded/"
    install -m 644 "${OUTPUTDIR}/PrivacySandboxAttestationsPreloaded/manifest.json" "${STAGEDIR}/${INSTALLDIR}/PrivacySandboxAttestationsPreloaded/"
    install -m 644 "${OUTPUTDIR}/PrivacySandboxAttestationsPreloaded/privacy-sandbox-attestations.dat" "${STAGEDIR}/${INSTALLDIR}/PrivacySandboxAttestationsPreloaded/"
  fi

  # MEI Preload
  if [ -f "${OUTPUTDIR}/MEIPreload/manifest.json" ]; then
    install -m 755 -d "${STAGEDIR}/${INSTALLDIR}/MEIPreload/"
    install -m 644 "${OUTPUTDIR}/MEIPreload/manifest.json" "${STAGEDIR}/${INSTALLDIR}/MEIPreload/"
    install -m 644 "${OUTPUTDIR}/MEIPreload/preloaded_data.pb" "${STAGEDIR}/${INSTALLDIR}/MEIPreload/"
  fi

  # Widevine CDM.
  if [ -d "${OUTPUTDIR}/WidevineCdm" ]; then
    # No need to strip; libwidevinecdm.so starts out stripped.
    cp -a "${OUTPUTDIR}/WidevineCdm" "${STAGEDIR}/${INSTALLDIR}/"
    find "${STAGEDIR}/${INSTALLDIR}/WidevineCdm" -type d -exec chmod 755 '{}' \;
    find "${STAGEDIR}/${INSTALLDIR}/WidevineCdm" -type f -exec chmod 644 '{}' \;
    find "${STAGEDIR}/${INSTALLDIR}/WidevineCdm" -name libwidevinecdm.so \
      -exec chmod ${SHLIB_PERMS} '{}' \;
  fi

  # ANGLE
  if [ -f "${OUTPUTDIR}/libEGL.so" ]; then
    for file in libEGL.so libGLESv2.so; do
      strippedfile="${OUTPUTDIR}/${file}.stripped"
      install -m ${SHLIB_PERMS} "${strippedfile}" "${STAGEDIR}/${INSTALLDIR}/${file}"
    done
  fi

  # ANGLE's libvulkan library
  if [ -f "${OUTPUTDIR}/libvulkan.so.1" ]; then
    file="libvulkan.so.1"
    strippedfile="${OUTPUTDIR}/${file}.stripped"
    install -m ${SHLIB_PERMS} "${strippedfile}" "${STAGEDIR}/${INSTALLDIR}/${file}"
  fi

  # SwiftShader VK
  if [ -f "${OUTPUTDIR}/libvk_swiftshader.so" ]; then
    install -m 755 -d "${STAGEDIR}/${INSTALLDIR}/"
    file="libvk_swiftshader.so"
    strippedfile="${OUTPUTDIR}/${file}.stripped"
    install -m ${SHLIB_PERMS} "${strippedfile}" "${STAGEDIR}/${INSTALLDIR}/${file}"
    # Install the ICD json file to point ANGLE to libvk_swiftshader.so
    install -m 644 "${OUTPUTDIR}/vk_swiftshader_icd.json" "${STAGEDIR}/${INSTALLDIR}/"
  fi

  # Optimization Guide Internal
  if [ -f "${OUTPUTDIR}/liboptimization_guide_internal.so" ]; then
    file="liboptimization_guide_internal.so"
    strippedfile="${OUTPUTDIR}/${file}.stripped"
    install -m ${SHLIB_PERMS} "${strippedfile}" "${STAGEDIR}/${INSTALLDIR}/${file}"
  fi

  # QT shim
  if [ -f "${OUTPUTDIR}/libqt5_shim.so" ]; then
    file="libqt5_shim.so"
    strippedfile="${OUTPUTDIR}/${file}.stripped"
    install -m ${SHLIB_PERMS} "${strippedfile}" "${STAGEDIR}/${INSTALLDIR}/${file}"
  fi
  if [ -f "${OUTPUTDIR}/libqt6_shim.so" ]; then
    file="libqt6_shim.so"
    strippedfile="${OUTPUTDIR}/${file}.stripped"
    install -m ${SHLIB_PERMS} "${strippedfile}" "${STAGEDIR}/${INSTALLDIR}/${file}"
  fi

  # libc++
  if [ -f "${OUTPUTDIR}/lib/libc++.so" ]; then
    install -m 755 -d "${STAGEDIR}/${INSTALLDIR}/lib/"
    install -m ${SHLIB_PERMS} -s "${OUTPUTDIR}/lib/libc++.so" "${STAGEDIR}/${INSTALLDIR}/lib/"
  fi

  # default apps
  if [ -d "${OUTPUTDIR}/default_apps" ]; then
    cp -a "${OUTPUTDIR}/default_apps" "${STAGEDIR}/${INSTALLDIR}/"
    find "${STAGEDIR}/${INSTALLDIR}/default_apps" -type d -exec chmod 755 '{}' \;
    find "${STAGEDIR}/${INSTALLDIR}/default_apps" -type f -exec chmod 644 '{}' \;
  fi

  # CHROME_VERSION_EXTRA file containing the channel
  if [ "$BRANDING" = "google_chrome" ]; then
    echo "$CHANNEL" > "${STAGEDIR}/${INSTALLDIR}/CHROME_VERSION_EXTRA"
    chmod 644 "${STAGEDIR}${INSTALLDIR}/CHROME_VERSION_EXTRA"
  fi

  # launcher script and symlink
  process_template "${OUTPUTDIR}/installer/common/wrapper" \
    "${STAGEDIR}/${INSTALLDIR}/${PACKAGE}"
  chmod 755 "${STAGEDIR}/${INSTALLDIR}/${PACKAGE}"
  if [ ! -z "${PACKAGE_ORIG}" ]; then
    if [ ! -f "${STAGEDIR}/${INSTALLDIR}/${PACKAGE_ORIG}" ]; then
      ln -sn "${INSTALLDIR}/${PACKAGE}" \
        "${STAGEDIR}/${INSTALLDIR}/${PACKAGE_ORIG}"
    fi
  fi
  if [ ! -z "${USR_BIN_SYMLINK_NAME}" ]; then
    ln -snf "${INSTALLDIR}/${PACKAGE}" \
      "${STAGEDIR}/usr/bin/${USR_BIN_SYMLINK_NAME}"
  fi

  # app icons
  local icon_regex=".*product_logo_[0-9]\+\."
  if [ "$BRANDING" = "google_chrome" ]; then
    if [ "$CHANNEL" = "beta" ]; then
      icon_regex=".*product_logo_[0-9]\+_beta\."
    elif [ "$CHANNEL" = "unstable" ]; then
      icon_regex=".*product_logo_[0-9]\+_dev\."
    elif [ "$CHANNEL" = "canary" ]; then
      icon_regex=".*product_logo_[0-9]\+_canary\."
    fi
  fi
  LOGO_RESOURCES_PNG=$(find "${OUTPUTDIR}/installer/theme/" \
    -regextype sed -regex "${icon_regex}png" -printf "%f ")
  LOGO_RESOURCE_XPM=$(find "${OUTPUTDIR}/installer/theme/" \
    -regextype sed -regex "${icon_regex}xpm" -printf "%f")
  for logo in ${LOGO_RESOURCES_PNG} ${LOGO_RESOURCE_XPM}; do
    install -m 644 \
      "${OUTPUTDIR}/installer/theme/${logo}" \
      "${STAGEDIR}/${INSTALLDIR}/"
  done

  # desktop integration
  install -m 755 "${OUTPUTDIR}/xdg-mime" "${STAGEDIR}${INSTALLDIR}/"
  install -m 755 "${OUTPUTDIR}/xdg-settings" "${STAGEDIR}${INSTALLDIR}/"

  if [ "$BRANDING" = "google_chrome" ]; then
    process_template "${OUTPUTDIR}/installer/common/google-chrome.appdata.xml.template" \
      "${STAGEDIR}/usr/share/appdata/${PACKAGE}.appdata.xml"
    chmod 644 "${STAGEDIR}/usr/share/appdata/${PACKAGE}.appdata.xml"
  else
    install -m 644 "${OUTPUTDIR}/installer/common/chromium-browser.appdata.xml" \
      "${STAGEDIR}/usr/share/appdata/${PACKAGE}.appdata.xml"
  fi

  process_template "${OUTPUTDIR}/installer/common/desktop.template" \
    "${STAGEDIR}/usr/share/applications/${PACKAGE}.desktop"
  chmod 644 "${STAGEDIR}/usr/share/applications/${PACKAGE}.desktop"
  process_template "${OUTPUTDIR}/installer/common/default-app.template" \
    "${STAGEDIR}/usr/share/gnome-control-center/default-apps/${PACKAGE}.xml"
  chmod 644 "${STAGEDIR}/usr/share/gnome-control-center/default-apps/${PACKAGE}.xml"
  process_template "${OUTPUTDIR}/installer/common/default-app-block.template" \
    "${STAGEDIR}${INSTALLDIR}/default-app-block"
  chmod 644 "${STAGEDIR}${INSTALLDIR}/default-app-block"

  # documentation
  process_template "${OUTPUTDIR}/installer/common/manpage.1.in" \
    "${STAGEDIR}/usr/share/man/man1/${USR_BIN_SYMLINK_NAME}.1"
  gzip -9n "${STAGEDIR}/usr/share/man/man1/${USR_BIN_SYMLINK_NAME}.1"
  chmod 644 "${STAGEDIR}/usr/share/man/man1/${USR_BIN_SYMLINK_NAME}.1.gz"
  # The stable channel allows launching the app without the "-stable"
  # suffix like the other channels.  Create a linked man page for the
  # app-without-the-channel case.
  if [ ! -f "${STAGEDIR}/usr/share/man/man1/${PACKAGE}.1.gz" ]; then
    ln -s "${USR_BIN_SYMLINK_NAME}.1.gz" \
      "${STAGEDIR}/usr/share/man/man1/${PACKAGE}.1.gz"
  fi

  # Check to make sure all the ELF binaries are stripped.
  UNSTRIPPED_BINS=
  for elf in $(find "${STAGEDIR}/${INSTALLDIR}/" -type f | xargs file |
                grep ELF | grep "not stripped" | awk '{print $1;}' |
                sed 's/:$//'); do
    UNSTRIPPED_BINS="${UNSTRIPPED_BINS} ${elf}"
  done

  if [ -n "${UNSTRIPPED_BINS}" ]; then
    echo "ERROR: Found unstripped ELF files:${UNSTRIPPED_BINS}" 1>&2
    exit 1
  fi

  # Check to make sure no ELF binaries set RPATH.
  if [ "${TARGET_OS}" != "chromeos" ]; then
    RPATH_BINS=
    for elf in $(find "${STAGEDIR}/${INSTALLDIR}/" -type f | xargs file |
                   grep ELF | awk '{print $1;}' | sed 's/:$//'); do
      if readelf -d ${elf} | grep "(RPATH)" >/dev/null; then
        RPATH_BINS="${RPATH_BINS} $(basename ${elf})"
      fi
    done
    if [ -n "${RPATH_BINS}" ]; then
      echo "ERROR: Found binaries with RPATH set:${RPATH_BINS}" 1>&2
      exit 1
    fi
  fi

  # Make sure ELF binaries live in INSTALLDIR exclusively.
  ELF_OUTSIDE_INSTALLDIR=$(find "${STAGEDIR}/" -not -path \
    "${STAGEDIR}${INSTALLDIR}/*" -type f | xargs file -b |
                             grep -ce "^ELF" || true)
  if [ "${ELF_OUTSIDE_INSTALLDIR}" -ne 0 ]; then
    echo "ERROR: Found ${ELF_OUTSIDE_INSTALLDIR} ELF binaries" \
      "outside of ${INSTALLDIR}" 1>&2
    exit 1
  fi

  # official build should not relax permission check.
  on_cog=
  if [ "$IS_OFFICIAL_BUILD" -eq 0 ]; then
    # on Cog, permission is always 0664 or 0775
    case "$(pwd)" in
    /google/cog/cloud/*)
       on_cog=1
       echo "INFO: build on Cog. relax permission for group writable" 1>&2
       ;;
    esac
  fi

  # Verify file permissions.
  for file in $(find "${STAGEDIR}" -mindepth 1); do
    local actual_perms=$(stat -c "%a" "${file}")
    local file_type="$(file -b "${file}")"
    local base_name=$(basename "${file}")
    if [[ "${file_type}" = "directory"* ]]; then
      local expected_perms=755
    elif [[ "${file_type}" = *"symbolic link"* ]]; then
      if [[ "$(readlink ${file})" = "/"* ]]; then
        # Absolute symlink.
        local expect_exists="${STAGEDIR}/$(readlink "${file}")"
      else
        # Relative symlink.
        local expect_exists="$(dirname "${file}")/$(readlink "${file}")"
      fi
      if [ ! -f "${expect_exists}" ]; then
        echo "Broken symlink: ${file}" 1>&2
        exit 1
      fi
      local expected_perms=777
    elif [ "${base_name}" = "chrome-management-service" ]; then
      local expected_perms=755
    elif [ "${base_name}" = "chrome-sandbox" ]; then
      local expected_perms=4755
    elif [[ "${file_type}" = *"shell script"* ]]; then
      local expected_perms=755
    elif [[ "${file_type}" = ELF* ]]; then
      if [[ "${base_name}" = *".so" || "${base_name}" = *".so."[[:digit:]]* ]]; then
        local expected_perms=${SHLIB_PERMS}
      else
        local expected_perms=755
      fi
    else
      # Regular data file.
      local expected_perms=644
    fi
    if [ ${expected_perms} -ne ${actual_perms} ]; then
      if [ -z "$on_cog" ]; then
        echo Expected permissions on ${base_name} to be \
          ${expected_perms}, but they were ${actual_perms} 1>&2
        exit 1
      fi
      local relaxed_expected_perms=${expected_perms}

      case ${expected_perms} in
      4755) relaxed_expected_perms=775 ;;
      644) relaxed_expected_perms=664   ;;
      755) relaxed_expected_perms=775  ;;
      esac

      if [ ${relaxed_expected_perms} -ne ${actual_perms} ]; then
        echo Expected permissions on ${base_name} to be \
          ${expected_perms} or ${relaxed_expected_perms}, \
          but they were ${actual_perms} 1>&2
        exit 1
      fi
    fi
  done
}