1#!/bin/bash -p 2 3# Copyright (c) 2012 The Chromium Authors. All rights reserved. 4# Use of this source code is governed by a BSD-style license that can be 5# found in the LICENSE file. 6 7# usage: dmgdiffer.sh product_name old_dmg new_dmg patch_dmg 8# 9# dmgdiffer creates a disk image containing a binary update able to patch 10# a product originally distributed in old_dmg to the version in new_dmg. Much 11# of this script is generic, but the make_patch_fs function is specific to 12# a product: in this case, Google Chrome. 13# 14# This script operates by mounting old_dmg and new_dmg, creating a new 15# filesystem structure containing dirpatches generated by dirdiffer and 16# goobsdiff (which should be located in the same directory as this script), 17# and producing a disk image from that structure. 18# 19# The Chrome make_patch_fs function produces an disk image that is able to 20# update a single old version on any Keystone channel to a new version on a 21# specific Keystone channel (the Keystone channel associated with new_dmg). 22# Chrome's updates are split into two dirpatches: one updates the old 23# versioned directory to the new one, and the other updates the remainder of 24# the application. The versioned directory is split out from the rest because 25# it contains the bulk of the application and its name changes from version to 26# version, and dirdiffer/dirpatcher do not directly handle name changes. This 27# approach also allows the versioned directory dirpatch to be applied in-place 28# in most cases during an update, rather than relying on a temporary 29# directory. In order to allow a single update dmg to apply to an old version 30# on any Keystone channel, several small files are never distributed as diffs, 31# and only as full (possibly compressed) versions of the new files. These 32# files include the outer application's Info.plist which contains Keystone 33# channel information, and anything created or modified by code-signing the 34# outer application. 35# 36# Application of update disk images produced by this script is 37# product-specific. With updates managed by Keystone, the update disk images 38# can contain a .keystone_install script that is able to locate and update 39# the installed product. 40# 41# Exit codes: 42# 0 OK 43# 1 Unknown failure 44# 2 Incorrect number of parameters 45# 3 Input disk images do not exist 46# 4 Output disk image already exists 47# 5 Parent of output directory does not exist or is not a directory 48# 6 Could not mount old_dmg 49# 7 Could not mount new_dmg 50# 8 Could not create temporary patch filesystem directory 51# 9 Could not create disk image 52# 10 Could not read old application data 53# 11 Could not read new application data 54# 12 Old or new application sanity check failure 55# 13 Could not write the patch 56# 57# Exit codes in the range 21-40 are mapped to codes 1-20 as returned by the 58# first dirdiffer invocation. Codes 41-60 are mapped to codes 1-20 as returned 59# by the second. 60 61set -eu 62 63# Environment sanitization. Set a known-safe PATH. Clear environment variables 64# that might impact the interpreter's operation. The |bash -p| invocation 65# on the #! line takes the bite out of BASH_ENV, ENV, and SHELLOPTS (among 66# other features), but clearing them here ensures that they won't impact any 67# shell scripts used as utility programs. SHELLOPTS is read-only and can't be 68# unset, only unexported. 69export PATH="/usr/bin:/bin:/usr/sbin:/sbin" 70unset BASH_ENV CDPATH ENV GLOBIGNORE IFS POSIXLY_CORRECT 71export -n SHELLOPTS 72 73ME="$(basename "${0}")" 74readonly ME 75SCRIPT_DIR="$(dirname "${0}")" 76readonly SCRIPT_DIR 77readonly DIRDIFFER="${SCRIPT_DIR}/dirdiffer.sh" 78readonly PKG_DMG="${SCRIPT_DIR}/pkg-dmg" 79 80err() { 81 local error="${1}" 82 83 echo "${ME}: ${error}" >& 2 84} 85 86declare -a g_cleanup g_cleanup_mount_points 87cleanup() { 88 local status=${?} 89 90 trap - EXIT 91 trap '' HUP INT QUIT TERM 92 93 if [[ ${status} -ge 128 ]]; then 94 err "Caught signal $((${status} - 128))" 95 fi 96 97 if [[ "${#g_cleanup_mount_points[@]}" -gt 0 ]]; then 98 local mount_point 99 for mount_point in "${g_cleanup_mount_points[@]}"; do 100 hdiutil detach "${mount_point}" -force >& /dev/null || true 101 done 102 fi 103 104 if [[ "${#g_cleanup[@]}" -gt 0 ]]; then 105 rm -rf "${g_cleanup[@]}" 106 fi 107 108 exit ${status} 109} 110 111mount_dmg() { 112 local dmg="${1}" 113 local mount_point="${2}" 114 115 if ! hdiutil attach "${1}" -mountpoint "${2}" \ 116 -nobrowse -owners off > /dev/null; then 117 # set -e is in effect. return ${?} so that the caller can check the return 118 # code if desired, perhaps to print a more useful error message or to exit 119 # with a more precise status than would be possible here. 120 return ${?} 121 fi 122} 123 124# make_patch_fs is responsible for comparing the old and new disk images 125# mounted at old_fs and new_fs, respectively, and populating patch_fs with the 126# contents of what will become a disk image able to update old_fs to new_fs. 127# It then outputs a string which will be used as the volume name of the 128# patch_dmg. 129# 130# The entire patch contents are placed into a .patch directory to hide them 131# from ordinary view. The disk image will be given a volume name like 132# "Google Chrome 5.0.375.55-5.0.375.70" as an identifying aid, although 133# uniqueness is not important and users will never interact directly with 134# them. 135make_patch_fs() { 136 local product_name="${1}" 137 local old_fs="${2}" 138 local new_fs="${3}" 139 local patch_fs="${4}" 140 141 readonly APP_NAME="${product_name}.app" 142 readonly APP_NAME_RE="${product_name}\\.app" 143 readonly APP_PLIST="Contents/Info" 144 readonly APP_VERSION_KEY="CFBundleShortVersionString" 145 readonly APP_BUNDLEID_KEY="CFBundleIdentifier" 146 readonly KS_VERSION_KEY="KSVersion" 147 readonly KS_PRODUCT_KEY="KSProductID" 148 readonly KS_CHANNEL_KEY="KSChannelID" 149 readonly VERSIONS_DIR="Contents/Versions" 150 readonly BUILD_RE="^[0-9]+\\.[0-9]+\\.([0-9]+)\\.[0-9]+\$" 151 readonly MIN_BUILD=434 152 153 local product_url="http://www.google.com/chrome/" 154 if [[ "${product_name}" = "Google Chrome Canary" ]]; then 155 product_url="http://tools.google.com/dlpage/chromesxs" 156 fi 157 158 local old_app_path="${old_fs}/${APP_NAME}" 159 local old_app_plist="${old_app_path}/${APP_PLIST}" 160 local old_app_version 161 if ! old_app_version="$(defaults read "${old_app_plist}" \ 162 "${APP_VERSION_KEY}")"; then 163 err "could not read old app version" 164 exit 10 165 fi 166 if ! [[ "${old_app_version}" =~ ${BUILD_RE} ]]; then 167 err "old app version not of expected format" 168 exit 10 169 fi 170 local old_app_version_build="${BASH_REMATCH[1]}" 171 172 local old_app_bundleid 173 if ! old_app_bundleid="$(defaults read "${old_app_plist}" \ 174 "${APP_BUNDLEID_KEY}")"; then 175 err "could not read old app bundle ID" 176 exit 10 177 fi 178 179 local old_ks_plist="${old_app_plist}" 180 local old_ks_version 181 if ! old_ks_version="$(defaults read "${old_ks_plist}" \ 182 "${KS_VERSION_KEY}")"; then 183 err "could not read old Keystone version" 184 exit 10 185 fi 186 187 local new_app_path="${new_fs}/${APP_NAME}" 188 local new_app_plist="${new_app_path}/${APP_PLIST}" 189 local new_app_version 190 if ! new_app_version="$(defaults read "${new_app_plist}" \ 191 "${APP_VERSION_KEY}")"; then 192 err "could not read new app version" 193 exit 11 194 fi 195 if ! [[ "${new_app_version}" =~ ${BUILD_RE} ]]; then 196 err "new app version not of expected format" 197 exit 11 198 fi 199 local new_app_version_build="${BASH_REMATCH[1]}" 200 201 local new_ks_plist="${new_app_plist}" 202 local new_ks_version 203 if ! new_ks_version="$(defaults read "${new_ks_plist}" \ 204 "${KS_VERSION_KEY}")"; then 205 err "could not read new Keystone version" 206 exit 11 207 fi 208 209 local new_ks_product 210 if ! new_ks_product="$(defaults read "${new_app_plist}" \ 211 "${KS_PRODUCT_KEY}")"; then 212 err "could not read new Keystone product ID" 213 exit 11 214 fi 215 216 if [[ ${old_app_version_build} -lt ${MIN_BUILD} ]] || 217 [[ ${new_app_version_build} -lt ${MIN_BUILD} ]]; then 218 err "old and new versions must be build ${MIN_BUILD} or newer" 219 exit 12 220 fi 221 222 local new_ks_channel 223 new_ks_channel="$(defaults read "${new_app_plist}" \ 224 "${KS_CHANNEL_KEY}" 2> /dev/null || true)" 225 226 local name_extra 227 if [[ "${new_ks_channel}" = "beta" ]]; then 228 name_extra=" Beta" 229 elif [[ "${new_ks_channel}" = "dev" ]]; then 230 name_extra=" Dev" 231 elif [[ "${new_ks_channel}" = "canary" ]]; then 232 name_extra= 233 elif [[ -n "${new_ks_channel}" ]]; then 234 name_extra=" ${new_ks_channel}" 235 fi 236 237 local old_versioned_dir="${old_app_path}/${VERSIONS_DIR}/${old_app_version}" 238 local new_versioned_dir="${new_app_path}/${VERSIONS_DIR}/${new_app_version}" 239 240 if ! cp -p "${SCRIPT_DIR}/keystone_install.sh" \ 241 "${patch_fs}/.keystone_install"; then 242 err "could not copy .keystone_install" 243 exit 13 244 fi 245 246 local patch_dotpatch_dir="${patch_fs}/.patch" 247 if ! mkdir "${patch_dotpatch_dir}"; then 248 err "could not mkdir patch_dotpatch_dir" 249 exit 13 250 fi 251 252 if ! cp -p "${SCRIPT_DIR}/dirpatcher.sh" \ 253 "${SCRIPT_DIR}/goobspatch" \ 254 "${SCRIPT_DIR}/liblzma_decompress.dylib" \ 255 "${SCRIPT_DIR}/xzdec" \ 256 "${patch_dotpatch_dir}/"; then 257 err "could not copy patching tools" 258 exit 13 259 fi 260 261 if ! echo "${new_ks_product}" > "${patch_dotpatch_dir}/ks_product" || 262 ! echo "${old_app_version}" > "${patch_dotpatch_dir}/old_app_version" || 263 ! echo "${new_app_version}" > "${patch_dotpatch_dir}/new_app_version" || 264 ! echo "${old_ks_version}" > "${patch_dotpatch_dir}/old_ks_version" || 265 ! echo "${new_ks_version}" > "${patch_dotpatch_dir}/new_ks_version"; then 266 err "could not write patch product or version information" 267 exit 13 268 fi 269 local patch_ks_channel_file="${patch_dotpatch_dir}/ks_channel" 270 if [[ -n "${new_ks_channel}" ]]; then 271 if ! echo "${new_ks_channel}" > "${patch_ks_channel_file}"; then 272 err "could not write Keystone channel information" 273 exit 13 274 fi 275 else 276 if ! touch "${patch_ks_channel_file}"; then 277 err "could not write empty Keystone channel information" 278 exit 13 279 fi 280 fi 281 282 # The only visible contents of the disk image will be a README file that 283 # explains the image's purpose. 284 local new_app_version_extra="${new_app_version}${name_extra}" 285 cat > "${patch_fs}/README.txt" << __EOF__ || \ 286 (err "could not write README.txt" && exit 13) 287This disk image contains a differential updater that can update 288${product_name} from version ${old_app_version} to ${new_app_version_extra}. 289 290This image is part of the auto-update system and is not independently 291useful. 292 293To install ${product_name}, please visit 294<${product_url}>. 295__EOF__ 296 297 local patch_versioned_dir="\ 298${patch_dotpatch_dir}/version_${old_app_version}_${new_app_version}.dirpatch" 299 300 if ! "${DIRDIFFER}" "${old_versioned_dir}" \ 301 "${new_versioned_dir}" \ 302 "${patch_versioned_dir}"; then 303 local status=${?} 304 err "could not create a dirpatch for the versioned directory" 305 exit $((${status} + 20)) 306 fi 307 308 # Set DIRDIFFER_EXCLUDE to exclude the contents of the Versions directory, 309 # but to include an empty Versions directory. The versioned directory was 310 # already addressed in the preceding dirpatch. 311 export DIRDIFFER_EXCLUDE="/${APP_NAME_RE}/Contents/Versions/" 312 313 # Set DIRDIFFER_NO_DIFF to exclude files introduced by or modified by 314 # Keystone channel and brand tagging and subsequent code signing. 315 export DIRDIFFER_NO_DIFF="\ 316/${APP_NAME_RE}/Contents/\ 317(CodeResources|Info\\.plist|MacOS/${product_name}|_CodeSignature/.*)$" 318 319 local patch_app_dir="${patch_dotpatch_dir}/application.dirpatch" 320 321 if ! "${DIRDIFFER}" "${old_app_path}" \ 322 "${new_app_path}" \ 323 "${patch_app_dir}"; then 324 local status=${?} 325 err "could not create a dirpatch for the application directory" 326 exit $((${status} + 40)) 327 fi 328 329 unset DIRDIFFER_EXCLUDE DIRDIFFER_NO_DIFF 330 331 echo "${product_name} ${old_app_version}-${new_app_version_extra} Update" 332} 333 334# package_patch_dmg creates a disk image at patch_dmg with the contents of 335# patch_fs. The disk image's volume name is taken from volume_name. temp_dir 336# is a work directory such as /tmp for the packager's use. 337package_patch_dmg() { 338 local patch_fs="${1}" 339 local patch_dmg="${2}" 340 local volume_name="${3}" 341 local temp_dir="${4}" 342 343 # Because most of the contents of ${patch_fs} are already compressed, the 344 # overall compression on the disk image is mostly used to minimize the sizes 345 # of the filesystem structures. In the presence of so much 346 # already-compressed data, zlib performs better than bzip2, so use UDZO. 347 if ! "${PKG_DMG}" \ 348 --verbosity 0 \ 349 --source "${patch_fs}" \ 350 --target "${patch_dmg}" \ 351 --tempdir "${temp_dir}" \ 352 --format UDZO \ 353 --volname "${volume_name}" \ 354 --config "openfolder_bless=0"; then 355 err "disk image creation failed" 356 exit 9 357 fi 358} 359 360# make_patch_dmg mounts old_dmg and new_dmg, invokes make_patch_fs to prepare 361# a patch filesystem, and then hands the patch filesystem to package_patch_dmg 362# to create patch_dmg. 363make_patch_dmg() { 364 local product_name="${1}" 365 local old_dmg="${2}" 366 local new_dmg="${3}" 367 local patch_dmg="${4}" 368 369 local temp_dir 370 temp_dir="$(mktemp -d -t "${ME}")" 371 g_cleanup+=("${temp_dir}") 372 373 local old_mount_point="${temp_dir}/old" 374 g_cleanup_mount_points+=("${old_mount_point}") 375 if ! mount_dmg "${old_dmg}" "${old_mount_point}"; then 376 err "could not mount old_dmg ${old_dmg}" 377 exit 6 378 fi 379 380 local new_mount_point="${temp_dir}/new" 381 g_cleanup_mount_points+=("${new_mount_point}") 382 if ! mount_dmg "${new_dmg}" "${new_mount_point}"; then 383 err "could not mount new_dmg ${new_dmg}" 384 exit 7 385 fi 386 387 local patch_fs="${temp_dir}/patch" 388 if ! mkdir "${patch_fs}"; then 389 err "could not mkdir patch_fs ${patch_fs}" 390 exit 8 391 fi 392 393 local volume_name 394 volume_name="$(make_patch_fs "${product_name}" \ 395 "${old_mount_point}" \ 396 "${new_mount_point}" \ 397 "${patch_fs}")" 398 399 hdiutil detach "${new_mount_point}" > /dev/null 400 unset g_cleanup_mount_points[${#g_cleanup_mount_points[@]}] 401 402 hdiutil detach "${old_mount_point}" > /dev/null 403 unset g_cleanup_mount_points[${#g_cleanup_mount_points[@]}] 404 405 package_patch_dmg "${patch_fs}" "${patch_dmg}" "${volume_name}" "${temp_dir}" 406 407 rm -rf "${temp_dir}" 408 unset g_cleanup[${#g_cleanup[@]}] 409} 410 411# shell_safe_path ensures that |path| is safe to pass to tools as a 412# command-line argument. If the first character in |path| is "-", "./" is 413# prepended to it. The possibly-modified |path| is output. 414shell_safe_path() { 415 local path="${1}" 416 if [[ "${path:0:1}" = "-" ]]; then 417 echo "./${path}" 418 else 419 echo "${path}" 420 fi 421} 422 423usage() { 424 echo "usage: ${ME} product_name old_dmg new_dmg patch_dmg" >& 2 425} 426 427main() { 428 local product_name old_dmg new_dmg patch_dmg 429 product_name="${1}" 430 old_dmg="$(shell_safe_path "${2}")" 431 new_dmg="$(shell_safe_path "${3}")" 432 patch_dmg="$(shell_safe_path "${4}")" 433 434 trap cleanup EXIT HUP INT QUIT TERM 435 436 if ! [[ -f "${old_dmg}" ]] || ! [[ -f "${new_dmg}" ]]; then 437 err "old_dmg and new_dmg must exist and be files" 438 usage 439 exit 3 440 fi 441 442 if [[ -e "${patch_dmg}" ]]; then 443 err "patch_dmg must not exist" 444 usage 445 exit 4 446 fi 447 448 local patch_dmg_parent 449 patch_dmg_parent="$(dirname "${patch_dmg}")" 450 if ! [[ -d "${patch_dmg_parent}" ]]; then 451 err "patch_dmg parent directory must exist and be a directory" 452 usage 453 exit 5 454 fi 455 456 make_patch_dmg "${product_name}" "${old_dmg}" "${new_dmg}" "${patch_dmg}" 457 458 trap - EXIT 459} 460 461if [[ ${#} -ne 4 ]]; then 462 usage 463 exit 2 464fi 465 466main "${@}" 467exit ${?} 468