OLD | NEW |
(Empty) | |
| 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_keychain_reauthorize_dir="${patch_fs}/.keychain_reauthorize" |
| 247 if ! mkdir "${patch_keychain_reauthorize_dir}"; then |
| 248 err "could not mkdir patch_keychain_reauthorize_dir" |
| 249 exit 13 |
| 250 fi |
| 251 |
| 252 if ! cp -p "${SCRIPT_DIR}/.keychain_reauthorize/${old_app_bundleid}" \ |
| 253 "${patch_keychain_reauthorize_dir}/${old_app_bundleid}"; then |
| 254 err "could not copy keychain_reauthorize" |
| 255 exit 13 |
| 256 fi |
| 257 |
| 258 local patch_dotpatch_dir="${patch_fs}/.patch" |
| 259 if ! mkdir "${patch_dotpatch_dir}"; then |
| 260 err "could not mkdir patch_dotpatch_dir" |
| 261 exit 13 |
| 262 fi |
| 263 |
| 264 if ! cp -p "${SCRIPT_DIR}/dirpatcher.sh" \ |
| 265 "${SCRIPT_DIR}/goobspatch" \ |
| 266 "${SCRIPT_DIR}/liblzma_decompress.dylib" \ |
| 267 "${SCRIPT_DIR}/xzdec" \ |
| 268 "${patch_dotpatch_dir}/"; then |
| 269 err "could not copy patching tools" |
| 270 exit 13 |
| 271 fi |
| 272 |
| 273 if ! echo "${new_ks_product}" > "${patch_dotpatch_dir}/ks_product" || |
| 274 ! echo "${old_app_version}" > "${patch_dotpatch_dir}/old_app_version" || |
| 275 ! echo "${new_app_version}" > "${patch_dotpatch_dir}/new_app_version" || |
| 276 ! echo "${old_ks_version}" > "${patch_dotpatch_dir}/old_ks_version" || |
| 277 ! echo "${new_ks_version}" > "${patch_dotpatch_dir}/new_ks_version"; then |
| 278 err "could not write patch product or version information" |
| 279 exit 13 |
| 280 fi |
| 281 local patch_ks_channel_file="${patch_dotpatch_dir}/ks_channel" |
| 282 if [[ -n "${new_ks_channel}" ]]; then |
| 283 if ! echo "${new_ks_channel}" > "${patch_ks_channel_file}"; then |
| 284 err "could not write Keystone channel information" |
| 285 exit 13 |
| 286 fi |
| 287 else |
| 288 if ! touch "${patch_ks_channel_file}"; then |
| 289 err "could not write empty Keystone channel information" |
| 290 exit 13 |
| 291 fi |
| 292 fi |
| 293 |
| 294 # The only visible contents of the disk image will be a README file that |
| 295 # explains the image's purpose. |
| 296 local new_app_version_extra="${new_app_version}${name_extra}" |
| 297 cat > "${patch_fs}/README.txt" << __EOF__ || \ |
| 298 (err "could not write README.txt" && exit 13) |
| 299 This disk image contains a differential updater that can update |
| 300 ${product_name} from version ${old_app_version} to ${new_app_version_extra}. |
| 301 |
| 302 This image is part of the auto-update system and is not independently |
| 303 useful. |
| 304 |
| 305 To install ${product_name}, please visit |
| 306 <${product_url}>. |
| 307 __EOF__ |
| 308 |
| 309 local patch_versioned_dir="\ |
| 310 ${patch_dotpatch_dir}/version_${old_app_version}_${new_app_version}.dirpatch" |
| 311 |
| 312 if ! "${DIRDIFFER}" "${old_versioned_dir}" \ |
| 313 "${new_versioned_dir}" \ |
| 314 "${patch_versioned_dir}"; then |
| 315 local status=${?} |
| 316 err "could not create a dirpatch for the versioned directory" |
| 317 exit $((${status} + 20)) |
| 318 fi |
| 319 |
| 320 # Set DIRDIFFER_EXCLUDE to exclude the contents of the Versions directory, |
| 321 # but to include an empty Versions directory. The versioned directory was |
| 322 # already addressed in the preceding dirpatch. |
| 323 export DIRDIFFER_EXCLUDE="/${APP_NAME_RE}/Contents/Versions/" |
| 324 |
| 325 # Set DIRDIFFER_NO_DIFF to exclude files introduced by or modified by |
| 326 # Keystone channel and brand tagging and subsequent code signing. |
| 327 export DIRDIFFER_NO_DIFF="\ |
| 328 /${APP_NAME_RE}/Contents/\ |
| 329 (CodeResources|Info\\.plist|MacOS/${product_name}|_CodeSignature/.*)$" |
| 330 |
| 331 local patch_app_dir="${patch_dotpatch_dir}/application.dirpatch" |
| 332 |
| 333 if ! "${DIRDIFFER}" "${old_app_path}" \ |
| 334 "${new_app_path}" \ |
| 335 "${patch_app_dir}"; then |
| 336 local status=${?} |
| 337 err "could not create a dirpatch for the application directory" |
| 338 exit $((${status} + 40)) |
| 339 fi |
| 340 |
| 341 unset DIRDIFFER_EXCLUDE DIRDIFFER_NO_DIFF |
| 342 |
| 343 echo "${product_name} ${old_app_version}-${new_app_version_extra} Update" |
| 344 } |
| 345 |
| 346 # package_patch_dmg creates a disk image at patch_dmg with the contents of |
| 347 # patch_fs. The disk image's volume name is taken from volume_name. temp_dir |
| 348 # is a work directory such as /tmp for the packager's use. |
| 349 package_patch_dmg() { |
| 350 local patch_fs="${1}" |
| 351 local patch_dmg="${2}" |
| 352 local volume_name="${3}" |
| 353 local temp_dir="${4}" |
| 354 |
| 355 # Because most of the contents of ${patch_fs} are already compressed, the |
| 356 # overall compression on the disk image is mostly used to minimize the sizes |
| 357 # of the filesystem structures. In the presence of so much |
| 358 # already-compressed data, zlib performs better than bzip2, so use UDZO. |
| 359 if ! "${PKG_DMG}" \ |
| 360 --verbosity 0 \ |
| 361 --source "${patch_fs}" \ |
| 362 --target "${patch_dmg}" \ |
| 363 --tempdir "${temp_dir}" \ |
| 364 --format UDZO \ |
| 365 --volname "${volume_name}" \ |
| 366 --config "openfolder_bless=0"; then |
| 367 err "disk image creation failed" |
| 368 exit 9 |
| 369 fi |
| 370 } |
| 371 |
| 372 # make_patch_dmg mounts old_dmg and new_dmg, invokes make_patch_fs to prepare |
| 373 # a patch filesystem, and then hands the patch filesystem to package_patch_dmg |
| 374 # to create patch_dmg. |
| 375 make_patch_dmg() { |
| 376 local product_name="${1}" |
| 377 local old_dmg="${2}" |
| 378 local new_dmg="${3}" |
| 379 local patch_dmg="${4}" |
| 380 |
| 381 local temp_dir |
| 382 temp_dir="$(mktemp -d -t "${ME}")" |
| 383 g_cleanup+=("${temp_dir}") |
| 384 |
| 385 local old_mount_point="${temp_dir}/old" |
| 386 g_cleanup_mount_points+=("${old_mount_point}") |
| 387 if ! mount_dmg "${old_dmg}" "${old_mount_point}"; then |
| 388 err "could not mount old_dmg ${old_dmg}" |
| 389 exit 6 |
| 390 fi |
| 391 |
| 392 local new_mount_point="${temp_dir}/new" |
| 393 g_cleanup_mount_points+=("${new_mount_point}") |
| 394 if ! mount_dmg "${new_dmg}" "${new_mount_point}"; then |
| 395 err "could not mount new_dmg ${new_dmg}" |
| 396 exit 7 |
| 397 fi |
| 398 |
| 399 local patch_fs="${temp_dir}/patch" |
| 400 if ! mkdir "${patch_fs}"; then |
| 401 err "could not mkdir patch_fs ${patch_fs}" |
| 402 exit 8 |
| 403 fi |
| 404 |
| 405 local volume_name |
| 406 volume_name="$(make_patch_fs "${product_name}" \ |
| 407 "${old_mount_point}" \ |
| 408 "${new_mount_point}" \ |
| 409 "${patch_fs}")" |
| 410 |
| 411 hdiutil detach "${new_mount_point}" > /dev/null |
| 412 unset g_cleanup_mount_points[${#g_cleanup_mount_points[@]}] |
| 413 |
| 414 hdiutil detach "${old_mount_point}" > /dev/null |
| 415 unset g_cleanup_mount_points[${#g_cleanup_mount_points[@]}] |
| 416 |
| 417 package_patch_dmg "${patch_fs}" "${patch_dmg}" "${volume_name}" "${temp_dir}" |
| 418 |
| 419 rm -rf "${temp_dir}" |
| 420 unset g_cleanup[${#g_cleanup[@]}] |
| 421 } |
| 422 |
| 423 # shell_safe_path ensures that |path| is safe to pass to tools as a |
| 424 # command-line argument. If the first character in |path| is "-", "./" is |
| 425 # prepended to it. The possibly-modified |path| is output. |
| 426 shell_safe_path() { |
| 427 local path="${1}" |
| 428 if [[ "${path:0:1}" = "-" ]]; then |
| 429 echo "./${path}" |
| 430 else |
| 431 echo "${path}" |
| 432 fi |
| 433 } |
| 434 |
| 435 usage() { |
| 436 echo "usage: ${ME} product_name old_dmg new_dmg patch_dmg" >& 2 |
| 437 } |
| 438 |
| 439 main() { |
| 440 local product_name old_dmg new_dmg patch_dmg |
| 441 product_name="${1}" |
| 442 old_dmg="$(shell_safe_path "${2}")" |
| 443 new_dmg="$(shell_safe_path "${3}")" |
| 444 patch_dmg="$(shell_safe_path "${4}")" |
| 445 |
| 446 trap cleanup EXIT HUP INT QUIT TERM |
| 447 |
| 448 if ! [[ -f "${old_dmg}" ]] || ! [[ -f "${new_dmg}" ]]; then |
| 449 err "old_dmg and new_dmg must exist and be files" |
| 450 usage |
| 451 exit 3 |
| 452 fi |
| 453 |
| 454 if [[ -e "${patch_dmg}" ]]; then |
| 455 err "patch_dmg must not exist" |
| 456 usage |
| 457 exit 4 |
| 458 fi |
| 459 |
| 460 local patch_dmg_parent |
| 461 patch_dmg_parent="$(dirname "${patch_dmg}")" |
| 462 if ! [[ -d "${patch_dmg_parent}" ]]; then |
| 463 err "patch_dmg parent directory must exist and be a directory" |
| 464 usage |
| 465 exit 5 |
| 466 fi |
| 467 |
| 468 make_patch_dmg "${product_name}" "${old_dmg}" "${new_dmg}" "${patch_dmg}" |
| 469 |
| 470 trap - EXIT |
| 471 } |
| 472 |
| 473 if [[ ${#} -ne 4 ]]; then |
| 474 usage |
| 475 exit 2 |
| 476 fi |
| 477 |
| 478 main "${@}" |
| 479 exit ${?} |
OLD | NEW |