#!/bin/bash -p
set -eu
export PATH="/usr/bin:/bin:/usr/sbin:/sbin"
unset BASH_ENV CDPATH ENV GLOBIGNORE IFS POSIXLY_CORRECT
export -n SHELLOPTS
ME="$(basename "${0}")"
readonly ME
SCRIPT_DIR="$(dirname "${0}")"
readonly SCRIPT_DIR
readonly DIRDIFFER="${SCRIPT_DIR}/dirdiffer.sh"
readonly PKG_DMG="${SCRIPT_DIR}/pkg-dmg"
err() {
local error="${1}"
echo "${ME}: ${error}" >& 2
}
declare -a g_cleanup g_cleanup_mount_points
cleanup() {
local status=${?}
trap - EXIT
trap '' HUP INT QUIT TERM
if [[ ${status} -ge 128 ]]; then
err "Caught signal $((${status} - 128))"
fi
if [[ "${#g_cleanup_mount_points[@]}" -gt 0 ]]; then
local mount_point
for mount_point in "${g_cleanup_mount_points[@]}"; do
hdiutil detach "${mount_point}" -force >& /dev/null || true
done
fi
if [[ "${#g_cleanup[@]}" -gt 0 ]]; then
rm -rf "${g_cleanup[@]}"
fi
exit ${status}
}
mount_dmg() {
local dmg="${1}"
local mount_point="${2}"
if ! hdiutil attach "${1}" -mountpoint "${2}" \
-nobrowse -owners off > /dev/null; then
return ${?}
fi
}
make_patch_fs() {
local product_name="${1}"
local old_fs="${2}"
local new_fs="${3}"
local patch_fs="${4}"
readonly APP_NAME="${product_name}.app"
readonly APP_NAME_RE="${product_name}\\.app"
readonly APP_PLIST="Contents/Info"
readonly APP_VERSION_KEY="CFBundleShortVersionString"
readonly APP_BUNDLEID_KEY="CFBundleIdentifier"
readonly KS_VERSION_KEY="KSVersion"
readonly KS_PRODUCT_KEY="KSProductID"
readonly KS_CHANNEL_KEY="KSChannelID"
readonly VERSIONS_DIR="Contents/Versions"
readonly BUILD_RE="^[0-9]+\\.[0-9]+\\.([0-9]+)\\.[0-9]+\$"
readonly MIN_BUILD=434
local product_url="http://www.google.com/chrome/"
if [[ "${product_name}" = "Google Chrome Canary" ]]; then
product_url="http://tools.google.com/dlpage/chromesxs"
fi
local old_app_path="${old_fs}/${APP_NAME}"
local old_app_plist="${old_app_path}/${APP_PLIST}"
local old_app_version
if ! old_app_version="$(defaults read "${old_app_plist}" \
"${APP_VERSION_KEY}")"; then
err "could not read old app version"
exit 10
fi
if ! [[ "${old_app_version}" =~ ${BUILD_RE} ]]; then
err "old app version not of expected format"
exit 10
fi
local old_app_version_build="${BASH_REMATCH[1]}"
local old_app_bundleid
if ! old_app_bundleid="$(defaults read "${old_app_plist}" \
"${APP_BUNDLEID_KEY}")"; then
err "could not read old app bundle ID"
exit 10
fi
local old_ks_plist="${old_app_plist}"
local old_ks_version
if ! old_ks_version="$(defaults read "${old_ks_plist}" \
"${KS_VERSION_KEY}")"; then
err "could not read old Keystone version"
exit 10
fi
local new_app_path="${new_fs}/${APP_NAME}"
local new_app_plist="${new_app_path}/${APP_PLIST}"
local new_app_version
if ! new_app_version="$(defaults read "${new_app_plist}" \
"${APP_VERSION_KEY}")"; then
err "could not read new app version"
exit 11
fi
if ! [[ "${new_app_version}" =~ ${BUILD_RE} ]]; then
err "new app version not of expected format"
exit 11
fi
local new_app_version_build="${BASH_REMATCH[1]}"
local new_ks_plist="${new_app_plist}"
local new_ks_version
if ! new_ks_version="$(defaults read "${new_ks_plist}" \
"${KS_VERSION_KEY}")"; then
err "could not read new Keystone version"
exit 11
fi
local new_ks_product
if ! new_ks_product="$(defaults read "${new_app_plist}" \
"${KS_PRODUCT_KEY}")"; then
err "could not read new Keystone product ID"
exit 11
fi
if [[ ${old_app_version_build} -lt ${MIN_BUILD} ]] ||
[[ ${new_app_version_build} -lt ${MIN_BUILD} ]]; then
err "old and new versions must be build ${MIN_BUILD} or newer"
exit 12
fi
local new_ks_channel
new_ks_channel="$(defaults read "${new_app_plist}" \
"${KS_CHANNEL_KEY}" 2> /dev/null || true)"
local name_extra
if [[ "${new_ks_channel}" = "beta" ]]; then
name_extra=" Beta"
elif [[ "${new_ks_channel}" = "dev" ]]; then
name_extra=" Dev"
elif [[ "${new_ks_channel}" = "canary" ]]; then
name_extra=
elif [[ -n "${new_ks_channel}" ]]; then
name_extra=" ${new_ks_channel}"
fi
local old_versioned_dir="${old_app_path}/${VERSIONS_DIR}/${old_app_version}"
local new_versioned_dir="${new_app_path}/${VERSIONS_DIR}/${new_app_version}"
if ! cp -p "${SCRIPT_DIR}/keystone_install.sh" \
"${patch_fs}/.keystone_install"; then
err "could not copy .keystone_install"
exit 13
fi
local patch_keychain_reauthorize_dir="${patch_fs}/.keychain_reauthorize"
if ! mkdir "${patch_keychain_reauthorize_dir}"; then
err "could not mkdir patch_keychain_reauthorize_dir"
exit 13
fi
if ! cp -p "${SCRIPT_DIR}/.keychain_reauthorize/${old_app_bundleid}" \
"${patch_keychain_reauthorize_dir}/${old_app_bundleid}"; then
err "could not copy keychain_reauthorize"
exit 13
fi
local patch_dotpatch_dir="${patch_fs}/.patch"
if ! mkdir "${patch_dotpatch_dir}"; then
err "could not mkdir patch_dotpatch_dir"
exit 13
fi
if ! cp -p "${SCRIPT_DIR}/dirpatcher.sh" \
"${SCRIPT_DIR}/goobspatch" \
"${SCRIPT_DIR}/liblzma_decompress.dylib" \
"${SCRIPT_DIR}/xzdec" \
"${patch_dotpatch_dir}/"; then
err "could not copy patching tools"
exit 13
fi
if ! echo "${new_ks_product}" > "${patch_dotpatch_dir}/ks_product" ||
! echo "${old_app_version}" > "${patch_dotpatch_dir}/old_app_version" ||
! echo "${new_app_version}" > "${patch_dotpatch_dir}/new_app_version" ||
! echo "${old_ks_version}" > "${patch_dotpatch_dir}/old_ks_version" ||
! echo "${new_ks_version}" > "${patch_dotpatch_dir}/new_ks_version"; then
err "could not write patch product or version information"
exit 13
fi
local patch_ks_channel_file="${patch_dotpatch_dir}/ks_channel"
if [[ -n "${new_ks_channel}" ]]; then
if ! echo "${new_ks_channel}" > "${patch_ks_channel_file}"; then
err "could not write Keystone channel information"
exit 13
fi
else
if ! touch "${patch_ks_channel_file}"; then
err "could not write empty Keystone channel information"
exit 13
fi
fi
local new_app_version_extra="${new_app_version}${name_extra}"
cat > "${patch_fs}/README.txt" << __EOF__ || \
(err "could not write README.txt" && exit 13)
This disk image contains a differential updater that can update
${product_name} from version ${old_app_version} to ${new_app_version_extra}.
This image is part of the auto-update system and is not independently
useful.
To install ${product_name}, please visit
<${product_url}>.
__EOF__
local patch_versioned_dir="\
${patch_dotpatch_dir}/version_${old_app_version}_${new_app_version}.dirpatch"
if ! "${DIRDIFFER}" "${old_versioned_dir}" \
"${new_versioned_dir}" \
"${patch_versioned_dir}"; then
local status=${?}
err "could not create a dirpatch for the versioned directory"
exit $((${status} + 20))
fi
export DIRDIFFER_EXCLUDE="/${APP_NAME_RE}/Contents/Versions/"
export DIRDIFFER_NO_DIFF="\
/${APP_NAME_RE}/Contents/\
(CodeResources|Info\\.plist|MacOS/${product_name}|_CodeSignature/.*)$"
local patch_app_dir="${patch_dotpatch_dir}/application.dirpatch"
if ! "${DIRDIFFER}" "${old_app_path}" \
"${new_app_path}" \
"${patch_app_dir}"; then
local status=${?}
err "could not create a dirpatch for the application directory"
exit $((${status} + 40))
fi
unset DIRDIFFER_EXCLUDE DIRDIFFER_NO_DIFF
echo "${product_name} ${old_app_version}-${new_app_version_extra} Update"
}
package_patch_dmg() {
local patch_fs="${1}"
local patch_dmg="${2}"
local volume_name="${3}"
local temp_dir="${4}"
if ! "${PKG_DMG}" \
--verbosity 0 \
--source "${patch_fs}" \
--target "${patch_dmg}" \
--tempdir "${temp_dir}" \
--format UDZO \
--volname "${volume_name}" \
--config "openfolder_bless=0"; then
err "disk image creation failed"
exit 9
fi
}
make_patch_dmg() {
local product_name="${1}"
local old_dmg="${2}"
local new_dmg="${3}"
local patch_dmg="${4}"
local temp_dir
temp_dir="$(mktemp -d -t "${ME}")"
g_cleanup+=("${temp_dir}")
local old_mount_point="${temp_dir}/old"
g_cleanup_mount_points+=("${old_mount_point}")
if ! mount_dmg "${old_dmg}" "${old_mount_point}"; then
err "could not mount old_dmg ${old_dmg}"
exit 6
fi
local new_mount_point="${temp_dir}/new"
g_cleanup_mount_points+=("${new_mount_point}")
if ! mount_dmg "${new_dmg}" "${new_mount_point}"; then
err "could not mount new_dmg ${new_dmg}"
exit 7
fi
local patch_fs="${temp_dir}/patch"
if ! mkdir "${patch_fs}"; then
err "could not mkdir patch_fs ${patch_fs}"
exit 8
fi
local volume_name
volume_name="$(make_patch_fs "${product_name}" \
"${old_mount_point}" \
"${new_mount_point}" \
"${patch_fs}")"
hdiutil detach "${new_mount_point}" > /dev/null
unset g_cleanup_mount_points[${#g_cleanup_mount_points[@]}]
hdiutil detach "${old_mount_point}" > /dev/null
unset g_cleanup_mount_points[${#g_cleanup_mount_points[@]}]
package_patch_dmg "${patch_fs}" "${patch_dmg}" "${volume_name}" "${temp_dir}"
rm -rf "${temp_dir}"
unset g_cleanup[${#g_cleanup[@]}]
}
shell_safe_path() {
local path="${1}"
if [[ "${path:0:1}" = "-" ]]; then
echo "./${path}"
else
echo "${path}"
fi
}
usage() {
echo "usage: ${ME} product_name old_dmg new_dmg patch_dmg" >& 2
}
main() {
local product_name old_dmg new_dmg patch_dmg
product_name="${1}"
old_dmg="$(shell_safe_path "${2}")"
new_dmg="$(shell_safe_path "${3}")"
patch_dmg="$(shell_safe_path "${4}")"
trap cleanup EXIT HUP INT QUIT TERM
if ! [[ -f "${old_dmg}" ]] || ! [[ -f "${new_dmg}" ]]; then
err "old_dmg and new_dmg must exist and be files"
usage
exit 3
fi
if [[ -e "${patch_dmg}" ]]; then
err "patch_dmg must not exist"
usage
exit 4
fi
local patch_dmg_parent
patch_dmg_parent="$(dirname "${patch_dmg}")"
if ! [[ -d "${patch_dmg_parent}" ]]; then
err "patch_dmg parent directory must exist and be a directory"
usage
exit 5
fi
make_patch_dmg "${product_name}" "${old_dmg}" "${new_dmg}" "${patch_dmg}"
trap - EXIT
}
if [[ ${#} -ne 4 ]]; then
usage
exit 2
fi
main "${@}"
exit ${?}