Home | History | Annotate | Download | only in mac
      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