From 8204755d1364be5cfbdbb1695803e8c1f2ea24f6 Mon Sep 17 00:00:00 2001 From: Ben Soares Date: Fri, 16 Aug 2024 18:36:32 +0100 Subject: [PATCH] JAL-4428 sign_dmg.sh with bells on --- utils/osx_signing/sign_dmg.sh | 514 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 472 insertions(+), 42 deletions(-) diff --git a/utils/osx_signing/sign_dmg.sh b/utils/osx_signing/sign_dmg.sh index 1b3973c..dd98944 100755 --- a/utils/osx_signing/sign_dmg.sh +++ b/utils/osx_signing/sign_dmg.sh @@ -1,60 +1,490 @@ -#!/bin/bash +#!/usr/bin/env bash -if [[ "$GITDIR" == "" ]]; then - GITDIR=~/uod-development/jalview-builds/git/jalview -fi; +# These are the defaults if neither env vars or command line option are set +[ -z $GITDIR ] && GITDIR="~/uod-development/jalview-builds/git/jalview" +[ -z $DEVELOPERID ] && DEVELOPERID="Developer ID" +[ -z $JVER ] && JVER="8" +TMP="/tmp" +TMPDMG="signingDMG" +TESTARCH="|x64|aarch64|" +YES=0 +CLEANUP=0 +GITENTITLEMENTSPATH="utils/osx_signing/entitlements.txt" +NOCODESIGNING=0 +NOVOLUMEICON=0 +VOLUMEICONPATH="utils/channels/release/images/jalview-VolumeIcon.icns" +DEFAULTVOLUMEICONFILE=".VolumeIcon.icns" -if [[ "$DEVELOPERID" == "" ]]; then - DEVELOPERID="Developer ID" -fi; +usage() { + echo "Usage: $( basename $0 ) [-h] [[-g gitdir] | [-e entfile]] [-d devid] [[-a appname] [-v appver ] [-j arch] [-w jver] | [-i dmgfile]] [-O] [-o outputdmg] [-t tmpdir] [-s signingdmg] [-S] [-z icnsfile] [-Z] [-y] [-C]" + echo " " + echo " This script is used in the signing process of DMG disk image files for macOS." + echo " Either -g GITDIR or -e ENTFILE should be given." + echo " Either -i DMGFILE or all of -a APPNAME -v APPVER -j ARCH -w JVER should be given." + echo " Environment variables will be used if set and no options given." + echo " Precedence is in the order of command line option, environment variable (where indicated), default (where indicated)." + echo " " + echo " -h Show help" + echo " -g gitdir Use git directory gitdir (also uses GITDIR env variable, default '$ARG_G')" + echo " -e entfile Use entitlements file entfile (defaults to 'GITDIR/${GITENTITLEMENTSPATH}')" + echo " -d devid Use the Developer ID devid (also uses DEVELOPERID env variable, default '$ARG_D')." + echo " -a appname Use the Application name appname (defaults to the first .app name found on DMG volume)." + echo " -v appver Assume application version appver (also uses APPVER env variable. No default)." + echo " -j arch Use the JVM architecture arch (also uses ARCH env variable. No default, should be one of${TESTARCH//|/ })." + echo " -w jver Assume java version jver (also uses JVER env variable. Defaults to '1.8')." + echo " -i dmgfile Sign DMGFILE (also uses DMGFILE env variable. Defaults to a combination of GITDIR, APPNAME, APPVER, ARCH and JVER)." + echo " -t tmpdir Use temp directory tmpdir (default '/tmp')." + echo " -s signingdmg Use signingdmg as the temporary signing folder name (default 'signingDMG')." + echo " -S Don't perform any code signing." + echo " -z icnsfile Use icnsfile as the volume icon file (defaults to using existing '$DEFAULTVOLUMEICONFILE' file or 'GITDIR/${VOLUMEICONPATH}'." + echo " -Z Don't set the volume icon, even if it already exists." + echo " -O Overwrite the output DMG file if it already exists." + echo " -o outputdmg Output DMG file (defaults to existing dmgfile in a 'signed' sub-directory)." + echo " -y Assume 'yes' to all confirmation requests." + echo " -C Cleanup temporary folders for the given dmgfile (Prevents all other activities. Cleanup can be narrowed down with either -i or some/all of -a -v -j -w." +} -if [[ "$TMPDMG" == "" ]]; then - TMPDMG="signingDMG" -fi; +while getopts "hg:e:d:a:v:j:w:i:t:s:Sz:ZyCOo:" opt; do + case ${opt} in + h) + usage + exit + ;; + g) + GITDIR="${OPTARG}" + ;; + e) + ENTITLEMENTSFILE="${OPTARG}" + ;; + d) + DEVELOPERID="${OPTARG}" + ;; + a) + APPNAME="${OPTARG}" + ;; + v) + APPVER="${OPTARG}" + ;; + j) + ARCH="${OPTARG}" + ;; + w) + JVER="${OPTARG}" + WJVER="$JVER" + ;; + i) + DMGFILE="${OPTARG}" + ;; + o) + OUTPUTDMGFILE="${OPTARG}" + ;; + O) + OVERWRITE=1 + ;; + t) + TMP="${OPTARG}" + ;; + s) + TMPDMG="${OPTARG}" + ;; + S) + NOCODESIGNING=1 + ;; + z) + VOLUMEICON="${OPTARG}" + ;; + Z) + NOVOLUMEICON=1 + ;; + y) + YES=1 + ;; + C) + CLEANUP=1 + ;; + *) + echo "Unrecognised option. Run with -h for help." + exit 1 + ;; + esac +done -echo APPNAME $APPNAME like Jalview Test -echo doing ARCH $ARCH -echo using entitlements from $GITDIR -echo using key $DEVELOPERID +if [ "$CLEANUP" != 1 ]; then + # Now check GITDIR, ENTITLEMENTSFILE, DEVELOPERID, APPNAME, APPVER, ARCH, JVER, DMGFILE, TMP, TMPDMG -FAPPNAME="${APPNAME/ /\\ }" -FAPPNAMEESC="${APPNAME/ /\\\\\\ }" -FWAPP="${APPNAME/ [A-Za-z]*/}" -ARCHNAME="${APPNAME// /_}-${APPVER//\./_}-macos-$ARCH-java_$JVER" -DMGNAME="${APPNAME/ /_}-${APPVER//\./_}-macos-$ARCH-java_$JVER.dmg" -VOLNAME="${APPNAME// /_}\\ Installer\\ \\(${APPVER//\./_}\\ $ARCH\\ $JVER\\)" -VLNAME="${APPNAME// /_} Installer (${APPVER//\./_} $ARCH $JVER)" -BORINGVLNAME="${APPNAME} Installer" + # check entitlements setting and file exists + if [ -z "$ENTITLEMENTSFILE" ]; then + ENTITLEMENTSFILE="${GITDIR}/${GITENTITLEMENTSPATH}" + fi + if [ -z "$ENTITLEMENTSFILE" ]; then + echo "Must set an entitlements file with -e entfile or -g GITDIR (or with GITDIR env variable)." + echo "" + usage + exit 2 + fi + if [ ! -e "$ENTITLEMENTSFILE" ]; then + echo "Entitlements file '$ENTITLEMENTSFILE' doesn't exist" + exit 3 + fi + # check developer id + if [ -z "$DEVELOPERID" ]; then + echo "Must set a Developer ID with -d DEVELOPERID (or with env variable)." + echo "" + usage + exit 4 + fi + # check ARCH is set and valid + if [ ! -z "$ARCH" -a "${TESTARCH/|$ARCH|/}" = "$TESTARCH" ]; then # not a valid arch + echo "ARCH must be one of${TESTARCH//|/ }. Set with -a ARCH (or with env variable)." + echo "" + usage + exit 5 + fi -echo "will mount $DMGNAME as $VOLNAME" -if [[ -d $TMPDMG ]]; then - echo "'$TMPDMG' is in the way. Please delete it or set TMPDMG" - exit 1; + # check VOLUMEICON + USEVOLUMEICON=0 + if [ ! -z "$VOLUMEICON" ]; then + if [ ! -e "$VOLUMEICON" ]; then + echo "Volume icon is set to '$VOLUMEICON' but it does not exist. Use -Z to NOT set a volume icon." + exit 6 + fi + USEVOLUMEICON=1 + else + VOLUMEICON="${GITDIR}/${VOLUMEICONPATH}" + if [ ! -e "$VOLUMEICON" ]; then + VOLUMEICON="" + fi + USEVOLUMEICON=1 + fi + + # check DMGFILE or alternative component args, set DMGFILE and check it exists + if [ -z "$DMGFILE" -a \( -z "$APPNAME" -o -z "$APPVER" -o -z "$ARCH" -o -z "$JVER" \) ]; then + echo "Must set either -i DMGFILE or all of -a APPNAME -v APPVER -j ARCH -w JVER (or with env variables)." + echo "" + usage + exit 7 + fi +fi + +if [ -z "$DMGFILE" ]; then + if [ "$CLEANUP" = 1 ]; then + # we can use wildcards for cleanup + [ -z "$APPNAME" ] && WAPPNAME="jalview*" || WAPPNAME="$APPNAME" + [ -z "$APPVER" ] && WAPPVER="*" || WAPPVER="$APPVER" + [ -z "$ARCH" ] && WARCH="*" || WARCH="$ARCH" + [ -z "$WJVER" ] && WJVER="*" # JVER is never "" + DMGNAME="${WAPPNAME// /_}-${WAPPVER//\./_}-macos-${WARCH}-java_${WJVER}.dmg" + DMGFILE="/tmp/fictitious.dmg" + else + DMGNAME="${APPNAME// /_}-${APPVER//\./_}-macos-${ARCH}-java_${JVER}.dmg" + OLDJVER=$([ "$JVER" -lt 9 ] && echo "1.${JVER}" || echo "${JVER}" ) + DMGFILE="${GITDIR}/install4j/${OLDJVER}/${DMGNAME}" + fi +else + DMGNAME="$( basename "$DMGFILE" )" +fi + +DMGNAMELC=$(echo "${DMGNAME//[ .]/_}" | tr '[:upper:]' '[:lower:]') + +if [ -z "$DMGFILE" ]; then + echo "Must set a DMG disk image file with -i." + echo "" + usage + exit 8 +fi + +if [ "$CLEANUP" = 1 ]; then + TO_REMOVE="" + TEMPBASE="${TMP}/${DMGNAMELC}" +else + if [ ! -e "$DMGFILE" ]; then + echo "DMG disk image file '$DMGFILE' doesn't exist" + exit 9 + fi + TEMPDIR=$(mktemp -d -p "${TMP}" -t "${DMGNAMELC}") + DMGDIR=$(dirname "$DMGFILE") +fi + +if [ "$CLEANUP" = 1 ]; then + echo "* -- Removing leftover temporary folders for DMG file '$DMGNAME' matching '${TEMPBASE}.*' ." + TO_REMOVE=$( ls -1d $TEMPBASE.* 2> /dev/null ) + + if [ -z "$TO_REMOVE" ]; then + echo "* Nothing to remove. Exiting." + exit + fi + + while IFS= read -r REMOVE; do + echo "* + will remove '${REMOVE}'" + done <<< "$TO_REMOVE" +else + echo "* -- Signing DMG file '$DMGFILE'" + echo "* -- Using entitlements file '$ENTITLEMENTSFILE'" + if [ "$NOCODESIGNING" = 1 ]; then + echo "* -- NOT actually code signing, but will still make new DMG file" + else + echo "* -- Using key '$DEVELOPERID'" + fi + echo "* -- Working in temp dir '$TEMPDIR'" +fi + +# Confirmation of what's about to happen +if [ "${YES}" != 1 ]; then + read -r -p "* Continue? [y/N] " response + case $(echo "${response}" | tr '[:upper:]' '[:lower:]') in + yes|y) + echo "* Continuing." + ;; + *) + echo "Aborting due to negative confirmation." + exit + ;; + esac +fi + +myexit() { + MSG=$1 + CODE=$2 + echo "$MSG" + exit $CODE +} + +mydetachexit() { + MSG=$1 + CODE=$2 + if [ ! -z "$VOLDIR" ]; then + echo "* First detaching '${VOLDIR}'" + hdiutil detach "$VOLDIR" + fi + echo "$MSG" + exit $CODE +} + +myparanoidrm() { + REMOVE="$1" + MSG="NOT REMOVING '${REMOVE}'" + # BE PARANOID BEFORE rm -Rf + TEST="${REMOVE}" + [ -z "$TEST" ] && myexit "${MSG}: Empty string." 20 + [ "$TEST" != "${TEST/../}" ] && myexit "${MSG}: Contains .. ." 21 + [ "$TEST" != "${TEST/\*/}" ] && myexit "${MSG}: Contains * ." 22 + [ "$TEST" = "${TEST#$TMP}" ] && myexit "${MSG}: Doesn't start with TMP='$TMP'." 23 + [ ${#TEST} -lt 10 ] && myexit "${MSG}: Too short." 24 + [ ! -e "$REMOVE" ] && myexit "${MSG}: Doesn't exist." 25 + + # check for mounted folders + REALPATH=$( realpath "$REMOVE" ) + MOUNT=$( hdiutil info | grep "Apple_HFS" | grep "$REALPATH" 2>/dev/null | head -1 | sed -e 's/^[^[:space:]]*[[:space:]]*Apple_HFS[[:space:]]*//' ) + if [ ! -z "$MOUNT" -a -d "$MOUNT" ]; then + echo "* First detaching '${MOUNT}'" + hdiutil detach "$MOUNT" + fi + + rm -Rf "${REMOVE}" +} + +if [ "$CLEANUP" = 1 ]; then + # just cleaning up + while IFS= read -r REMOVE; do + echo "* Removing '${REMOVE}'" + myparanoidrm "${REMOVE}" + done <<< "$TO_REMOVE" + echo "* Finished cleanup. Exiting." + exit +fi + +MOUNTROOT="${TEMPDIR}/Volume" +mkdir -p "$MOUNTROOT" + +echo "* Mounting disk image '${DMGFILE}' in '${MOUNTROOT}'" +echo "hdiutil attach -mountroot \"${MOUNTROOT}\" \"${DMGFILE}\"" +hdiutil attach -mountroot "${MOUNTROOT}" "${DMGFILE}" || myexit "Could not mount '${DMGFILE}' in '${MOUNTROOT}'. Aborting." 10 +VOLDIR=$(ls -1d "${MOUNTROOT%/}"/* | head -1) +VOLDIR="${VOLDIR%/}" # remove trailing slash +if [ -z "$VOLDIR" ]; then + myexit "Failed to find mounted volume in '${MOUNTROOT}'" 11 +fi +VOLNAME=$(basename "$VOLDIR") +FOUNDAPPNAME=$(ls -1d "${VOLDIR}/"*.app | head -1) +FOUNDAPPNAME="${FOUNDAPPNAME%/}" # remove trailing slash +FOUNDAPPNAME="${FOUNDAPPNAME%.app}" # without the ".app" +FOUNDAPPNAME="$(basename "$FOUNDAPPNAME")" + +echo "* -- Found volume name '${VOLNAME}'" +echo "* -- Found application name '${FOUNDAPPNAME}'" +TEMPDMGDIR="${TEMPDIR%/}/${TMPDMG}" +if [ -e "$TEMPDMGDIR" ]; then + mydetachexit "Folder '${TEMPDMGDIR}' already exists. Please remove it or use -s SIGNINGDMG to set a different dir name." 12 + exit 11 +fi +if [ "$FOUNDAPPNAME" != "$APPNAME" ]; then + if [ -z "$APPNAME" ]; then + echo "* ---- Going to set APPNAME to '${FOUNDAPPNAME}'" + else + echo "* ---- Going to reset APPNAME from '${APPNAME}' to '${FOUNDAPPNAME}'" + fi +fi +echo "* ---- Going to copy volume contents to '${TEMPDMGDIR}'" +if [ "$NOVOLUMEICON" != 1 ]; then + echo "* ---- Going to try and set a volume icon to '${VOLUMEICON}'" +fi + + +# Confirmation of reset to APPNAME +if [ "${YES}" != 1 ]; then + read -r -p "* Continue? [y/N] " response + case $(echo "${response}" | tr '[:upper:]' '[:lower:]') in + yes|y) + echo "* Continuing." + ;; + *) + mydetachexit "Aborting due to negative confirmation." 0 + exit + ;; + esac +fi + +APPNAME="$FOUNDAPPNAME" + +# Copy volume contents +echo "* Copying '${VOLDIR}' to '${TEMPDMGDIR}'" +ditto "$VOLDIR" "$TEMPDMGDIR" + +echo "* Unmounting '${VOLDIR}' and removing '${TEMPDIR}/Volume'" +hdiutil detach "$VOLDIR" +rmdir "${TEMPDIR}/Volume" + +TRUE="" +RUNNING="RUNNING: " +if [ "$NOCODESIGNING" = 1 ]; then + echo "* NO actual code signing due to -S flag" + TRUE="true" + RUNNING="NOT RUNNING: " +fi + + +echo "* Code signing in '${TEMPDMGDIR}'" + + +FILE="${TEMPDMGDIR}/${APPNAME}.app/Contents/Resources/app/jre/Contents/MacOS/libjli.dylib" +echo "* + '$FILE'" + +echo "${RUNNING}codesign --remove-signature --force --deep -vvvv -s \"$DEVELOPERID\" --options runtime --entitlements \"$ENTITLEMENTSFILE\" \"$FILE\"" +$TRUE codesign --remove-signature --force --deep -vvvv -s "$DEVELOPERID" --options runtime --entitlements "$ENTITLEMENTSFILE" "$FILE" + +echo "${RUNNING}codesign --verify --deep -v \"$FILE\"" +$TRUE codesign --verify --deep -v "$FILE" + + +FILE="${TEMPDMGDIR}/${APPNAME}.app/Contents/MacOS/JavaApplicationStub" +echo "* + '$FILE'" + +echo "${RUNNING}codesign --remove-signature --force --deep -vvvv -s \"$DEVELOPERID\" --options runtime --entitlements \"$ENTITLEMENTSFILE\" \"$FILE\"" +$TRUE codesign --remove-signature --force --deep -vvvv -s "$DEVELOPERID" --options runtime --entitlements "$ENTITLEMENTSFILE" "$FILE" + + +if [ ! -z "$OUTPUTDMGFILE" ]; then + NEWDMGFILE="$OUTPUTDMGFILE" +else + SIGNEDDIR="${DMGDIR%/}/signed" + NEWDMGFILE="${SIGNEDDIR}/${DMGNAME}" + echo "* Creating folder '${SIGNEDDIR}' for new DMG file '${DMGNAME}'" + mkdir -p "$SIGNEDDIR" +fi + +if [ -e "$NEWDMGFILE" ]; then + if [ "$OVERWRITE" = 1 ]; then + rm "$NEWDMGFILE" + else + mydetachexit "* New DMG file already exists. Use -O to overwrite. Exiting." 13 + fi fi -if [[ -f $DMGNAME ]]; then - hdiutil attach $DMGNAME - ditto /Volumes/${FWAPP}* $TMPDMG - hdiutil eject /Volumes/${FWAPP}* - mkdir -p unsigned - mv -v $DMGNAME unsigned/ - echo Moved $DMGNAME to unsigned/$DMGNAME - codesign --remove-signature --force --deep -vvvv -s "Developer ID" --options runtime --entitlements $GITDIR/utils/osx_signing/entitlements.txt $TMPDMG/${FWAPP}*.app/Contents/Resources/app/jre/Contents/MacOS/libjli.dylib - codesign --verify --deep -v ./$TMPDMG/${FWAPP}*.app/Contents/Resources/app/jre/Contents/MacOS/libjli.dylib +if [ "$NOVOLUMEICON" = 1 ]; then + echo "* NOT setting a volume icon" - codesign --remove-signature --force --deep -vvvv -s "Developer ID" --options runtime --entitlements $GITDIR/utils/osx_signing/entitlements.txt $TMPDMG/${FWAPP}*.app/Contents/MacOS/JavaApplicationStub + # without volume icon - hdiutil create -megabytes 260 -srcfolder ./$TMPDMG -volname "$BORINGVLNAME" $ARCHNAME.dmg + echo "* Creating new DMG file '${NEWDMGFILE}' to sign" - codesign --force --deep -vvvv -s "Developer ID" --options runtime --entitlements $GITDIR/utils/osx_signing/entitlements.txt $ARCHNAME.dmg + echo "hdiutil create -megabytes 260 -srcfolder \"$TEMPDMGDIR\" -volname \"$VOLNAME\" \"$NEWDMGFILE\"" + hdiutil create -megabytes 260 -srcfolder "$TEMPDMGDIR" -volname "$VOLNAME" "$NEWDMGFILE" || mydetachexit "Could not create new DMG file '${NEWDMGFILE}'" 15 - codesign --deep -vvvv $ARCHNAME.dmg - - rm -Rf $TMPDMG else - echo Can\'t find $DMGNAME - dit you set APPNAME APPVER ARCH and JVER correctly ? + + # with volume icon + + TEMP_RW_BASE=$(mktemp -d -p "${TEMPDIR}" -t "temp_rw") + TEMPDMGFILE="${TEMP_RW_BASE}.dmg" + TEMPMOUNTDIR="${TEMP_RW_BASE}_mount" + + + echo "* Creating temporary RW DMG file '${TMPDMGFILE}' to sign" + + echo "hdiutil create -format UDRW -megabytes 260 -srcfolder \"$TEMPDMGDIR\" -volname \"$VOLNAME\" \"$TEMPDMGFILE\"" + hdiutil create -format UDRW -megabytes 260 -srcfolder "$TEMPDMGDIR" -volname "$VOLNAME" "$TEMPDMGFILE" || mydetachexit "Could not create temporary DMG file '${TEMPDMGFILE}'" 15 + + + echo "* Mounting temporary disk image '${TEMPDMGFILE}' on '${TEMPMOUNTDIR}'" + + echo "hdiutil attach -mountpoint \"${TEMPMOUNTDIR}\" \"${TEMPDMGFILE}\"" + hdiutil attach -mountpoint "${TEMPMOUNTDIR}" "${TEMPDMGFILE}" || myexit "Could not mount '${TEMPDMGFILE}' on '${TEMPMOUNTDIR}'. Aborting." 16 + + VOLDIR="$TEMPMOUNTDIR" # for mydetachexit + + + if [ ! -z "$VOLUMEICON" -a -e "$VOLUMEICON" -a "$USEVOLUMEICON" = 1 ]; then + + echo "* Copying the volume icon '${VOLUMEICON}' to the temporary volume" + cp -f "$VOLUMEICON" "${TEMPMOUNTDIR}/${DEFAULTVOLUMEICONFILE}" + + fi + if [ -e "${TEMPMOUNTDIR}/${DEFAULTVOLUMEICONFILE}" ]; then + + echo "* Setting the volume icon '${DEFAULTVOLUMEICONFILE}' to the volume" + + echo "SetFile -c icnC \"${TEMPMOUNTDIR}/${DEFAULTVOLUMEICONFILE}\"" + SetFile -c icnC "${TEMPMOUNTDIR}/${DEFAULTVOLUMEICONFILE}" + + echo "SetFile -a C \"${TEMPMOUNTDIR}\"" + SetFile -a C "${TEMPMOUNTDIR}" + + else + echo "* Could not find volume icon '${VOLUMEICON}'. Not setting a volume icon." + fi + + + echo "* Unmounting '${TEMPMOUNTDIR}'" + + echo "hdiutil detach \"$TEMPMOUNTDIR\"" + hdiutil detach "$TEMPMOUNTDIR" + + + echo "* Converting temporary DMG file to new DMG file '${NEWDMGFILE}' to sign" + + echo "hdiutil convert \"$TEMPDMGFILE\" -format UDZO -o \"$NEWDMGFILE\"" + hdiutil convert "$TEMPDMGFILE" -format UDZO -o "$NEWDMGFILE" || mydetachexit "Could not convert to new DMG file '${NEWDMGFILE}'" 17 + + + echo "* Removing temporary DMG file '${TEMPDMGFILE}'" + rm "$TEMPDMGFILE" + fi + +echo "* Code signing '${NEWDMGFILE}'" + +echo "${RUNNING}codesign --force --deep -vvvv -s \"$DEVELOPERID\" --options runtime --entitlements \"$ENTITLEMENTSFILE\" \"$NEWDMGFILE\"" +$TRUE codesign --force --deep -vvvv -s "$DEVELOPERID" --options runtime --entitlements "$ENTITLEMENTSFILE" "$NEWDMGFILE" + +echo "${RUNNING}codesign --deep -vvvv \"$NEWDMGFILE\"" +$TRUE codesign --deep -vvvv "$NEWDMGFILE" + + +echo "* Removing TEMPDIR '${TEMPDIR}'" +myparanoidrm "${TEMPDIR}" + +echo "*** Signed DMG file at '${NEWDMGFILE}'" -- 1.7.10.2