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 61 set -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. 69 export PATH="/usr/bin:/bin:/usr/sbin:/sbin" 70 unset BASH_ENV CDPATH ENV GLOBIGNORE IFS POSIXLY_CORRECT 71 export -n SHELLOPTS 72 73 ME="$(basename "${0}")" 74 readonly ME 75 SCRIPT_DIR="$(dirname "${0}")" 76 readonly SCRIPT_DIR 77 readonly DIRDIFFER="${SCRIPT_DIR}/dirdiffer.sh" 78 readonly PKG_DMG="${SCRIPT_DIR}/pkg-dmg" 79 80 err() { 81 local error="${1}" 82 83 echo "${ME}: ${error}" >& 2 84 } 85 86 declare -a g_cleanup g_cleanup_mount_points 87 cleanup() { 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 111 mount_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. 135 make_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) 287 This disk image contains a differential updater that can update 288 ${product_name} from version ${old_app_version} to ${new_app_version_extra}. 289 290 This image is part of the auto-update system and is not independently 291 useful. 292 293 To 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. 337 package_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. 363 make_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. 414 shell_safe_path() { 415 local path="${1}" 416 if [[ "${path:0:1}" = "-" ]]; then 417 echo "./${path}" 418 else 419 echo "${path}" 420 fi 421 } 422 423 usage() { 424 echo "usage: ${ME} product_name old_dmg new_dmg patch_dmg" >& 2 425 } 426 427 main() { 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 461 if [[ ${#} -ne 4 ]]; then 462 usage 463 exit 2 464 fi 465 466 main "${@}" 467 exit ${?} 468