| Index: chrome/installer/mac/keystone_install.sh | 
| =================================================================== | 
| --- chrome/installer/mac/keystone_install.sh	(revision 49726) | 
| +++ chrome/installer/mac/keystone_install.sh	(working copy) | 
| @@ -23,12 +23,19 @@ | 
| #  3  Basic sanity check destination failure (e.g. ticket points to nothing) | 
| #  4  Update driven by user ticket when a system ticket is also present | 
| #  5  Could not prepare existing installed version to receive update | 
| -#  6  rsync failed (could not assure presence of Versions directory) | 
| +#  6  Patch sanity check failure | 
| #  7  rsync failed (could not copy new versioned directory to Versions) | 
| #  8  rsync failed (could not update outer .app bundle) | 
| #  9  Could not get the version, update URL, or channel after update | 
| # 10  Updated application does not have the version number from the update | 
| # 11  ksadmin failure | 
| +# 12  dirpatcher failed for versioned directory | 
| +# 13  dirpatcher failed for outer .app bundle | 
| +# | 
| +# The following exit codes are not used by this script, but can be used to | 
| +# convey special meaning to Keystone: | 
| +# 66  (unused) success, request reboot | 
| +# 77  (unused) try installation again later | 
|  | 
| set -eu | 
|  | 
| @@ -74,6 +81,41 @@ | 
| fi | 
| } | 
|  | 
| +declare g_temp_dir | 
| +cleanup() { | 
| +  local status=${?} | 
| + | 
| +  trap - EXIT | 
| +  trap '' HUP INT QUIT TERM | 
| + | 
| +  if [[ ${status} -ge 128 ]]; then | 
| +    err "Caught signal $((${status} - 128))" | 
| +  fi | 
| + | 
| +  if [[ -n "${g_temp_dir}" ]]; then | 
| +    rm -rf "${g_temp_dir}" | 
| +  fi | 
| + | 
| +  exit ${status} | 
| +} | 
| + | 
| +ensure_temp_dir() { | 
| +  if [[ -z "${g_temp_dir}" ]]; then | 
| +    # Choose a template that won't be a dot directory.  Make it safe by | 
| +    # removing leading hyphens, too. | 
| +    local template="${ME}" | 
| +    if [[ "${template}" =~ ^[-.]+(.*)$ ]]; then | 
| +      template="${BASH_REMATCH[1]}" | 
| +    fi | 
| +    if [[ -z "${template}" ]]; then | 
| +      template="keystone_install" | 
| +    fi | 
| + | 
| +    g_temp_dir="$(mktemp -d -t "${template}")" | 
| +    note "g_temp_dir = ${g_temp_dir}" | 
| +  fi | 
| +} | 
| + | 
| # Returns 0 (true) if |symlink| exists, is a symbolic link, and appears | 
| # writable on the basis of its POSIX permissions.  This is used to determine | 
| # writability like test's -w primary, but -w resolves symbolic links and this | 
| @@ -184,6 +226,57 @@ | 
| return 0 | 
| } | 
|  | 
| +# ensure_writable_symlinks_recursive calls ensure_writable_symlink for every | 
| +# symbolic link in |directory|, recursivley. | 
| +# | 
| +# In some very weird and rare cases, it is possible to wind up with a user | 
| +# installation that contains symbolic links that the user does not have write | 
| +# permission over.  More on how that might happen later. | 
| +# | 
| +# If a weird and rare case like this is observed, rsync will exit with an | 
| +# error when attempting to update the times on these symbolic links.  rsync | 
| +# may not be intelligent enough to try creating a new symbolic link in these | 
| +# cases, but this script can be. | 
| +# | 
| +# The problem occurs when an administrative user first drag-installs the | 
| +# application to /Applications, resulting in the program's user being set to | 
| +# the user's own ID.  If, subsequently, a .pkg package is installed over that, | 
| +# the existing directory ownership will be preserved, but file ownership will | 
| +# be changed to whateer is specified by the package, typically root.  This | 
| +# applies to symbolic links as well.  On a subsequent update, rsync will be | 
| +# able to copy the new files into place, because the user still has permission | 
| +# to write to the directories.  If the symbolic link targets are not changing, | 
| +# though, rsync will not replace them, and they will remain owned by root. | 
| +# The user will not have permission to update the time on the symbolic links, | 
| +# resulting in an rsync error. | 
| +ensure_writable_symlinks_recursive() { | 
| +  local directory="${1}" | 
| + | 
| +  # This fix-up is not necessary when running as root, because root will | 
| +  # always be able to write everything needed. | 
| +  if [[ ${EUID} -eq 0 ]]; then | 
| +    return 0 | 
| +  fi | 
| + | 
| +  # This step isn't critical. | 
| +  local set_e= | 
| +  if [[ "${-}" =~ e ]]; then | 
| +    set_e="y" | 
| +    set +e | 
| +  fi | 
| + | 
| +  # Use find -print0 with read -d $'\0' to handle even the weirdest paths. | 
| +  local symlink | 
| +  while IFS= read -r -d $'\0' symlink; do | 
| +    ensure_writable_symlink "${symlink}" | 
| +  done < <(find "${directory}" -type l -print0) | 
| + | 
| +  # Go back to how things were. | 
| +  if [[ -n "${set_e}" ]]; then | 
| +    set -e | 
| +  fi | 
| +} | 
| + | 
| # Prints the version of ksadmin, as reported by ksadmin --ksadmin-version, to | 
| # stdout.  This function operates with "static" variables: it will only check | 
| # the ksadmin version once per script run.  If ksadmin is old enough to not | 
| @@ -208,11 +301,11 @@ | 
| # Keystone version.  |check_version| should be a string of the form | 
| # "major.minor.micro.build".  Returns 1 (false) if either |check_version| or | 
| # the Keystone version do not match this format. | 
| -readonly VER_RE="^([0-9]+)\\.([0-9]+)\\.([0-9]+)\\.([0-9]+)\$" | 
| +readonly KSADMIN_VERSION_RE="^([0-9]+)\\.([0-9]+)\\.([0-9]+)\\.([0-9]+)\$" | 
| is_ksadmin_version_ge() { | 
| local check_version="${1}" | 
|  | 
| -  if ! [[ "${check_version}" =~ ${VER_RE} ]]; then | 
| +  if ! [[ "${check_version}" =~ ${KSADMIN_VERSION_RE} ]]; then | 
| return 1 | 
| fi | 
|  | 
| @@ -224,7 +317,7 @@ | 
| local ksadmin_version | 
| ksadmin_version="$(ksadmin_version)" | 
|  | 
| -  if ! [[ "${ksadmin_version}" =~ ${VER_RE} ]]; then | 
| +  if ! [[ "${ksadmin_version}" =~ ${KSADMIN_VERSION_RE} ]]; then | 
| return 1 | 
| fi | 
|  | 
| @@ -294,10 +387,13 @@ | 
| # Early steps are critical.  Don't continue past any failure. | 
| set -e | 
|  | 
| +  trap cleanup EXIT HUP INT QUIT TERM | 
| + | 
| readonly PRODUCT_NAME="Google Chrome" | 
| readonly APP_DIR="${PRODUCT_NAME}.app" | 
| readonly FRAMEWORK_NAME="${PRODUCT_NAME} Framework" | 
| readonly FRAMEWORK_DIR="${FRAMEWORK_NAME}.framework" | 
| +  readonly PATCH_DIR=".patch" | 
| readonly CONTENTS_DIR="Contents" | 
| readonly APP_PLIST="${CONTENTS_DIR}/Info" | 
| readonly VERSIONS_DIR="${CONTENTS_DIR}/Versions" | 
| @@ -348,51 +444,144 @@ | 
| exit 2 | 
| fi | 
|  | 
| -  # The update to install. | 
| -  local update_app="${update_dmg_mount_point}/${APP_DIR}" | 
| -  note "update_app = ${update_app}" | 
| - | 
| -  # Make sure that there's something to copy from, and that it's an absolute | 
| -  # path. | 
| -  if [[ "${update_app:0:1}" != "/" ]] || | 
| -     ! [[ -d "${update_app}" ]]; then | 
| -    err "update_app must be an absolute path to a directory" | 
| +  local patch_dir="${update_dmg_mount_point}/${PATCH_DIR}" | 
| +  if [[ "${patch_dir:0:1}" != "/" ]]; then | 
| +    note "patch_dir = ${patch_dir}" | 
| +    err "patch_dir must be an absolute path" | 
| exit 2 | 
| fi | 
|  | 
| -  # Get some information about the update. | 
| -  note "reading update values" | 
| - | 
| -  local update_app_plist="${update_app}/${APP_PLIST}" | 
| -  note "update_app_plist = ${update_app_plist}" | 
| -  local update_version_app | 
| -  if ! update_version_app="$(defaults read "${update_app_plist}" \ | 
| -                                           "${APP_VERSION_KEY}")" || | 
| -     [[ -z "${update_version_app}" ]]; then | 
| -    err "couldn't determine update_version_app" | 
| +  # Figure out if this is an ordinary installation disk image being used as a | 
| +  # full update, or a patch.  A patch will have a .patch directory at the root | 
| +  # of the disk image containing information about the update, tools to apply | 
| +  # it, and the update contents. | 
| +  local is_patch= | 
| +  local dirpatcher= | 
| +  if [[ -d "${patch_dir}" ]]; then | 
| +    # patch_dir exists and is a directory - this is a patch update. | 
| +    is_patch="y" | 
| +    dirpatcher="${patch_dir}/dirpatcher.sh" | 
| +    if ! [[ -x "${dirpatcher}" ]]; then | 
| +      err "couldn't locate dirpatcher" | 
| +      exit 6 | 
| +    fi | 
| +  elif [[ -e "${patch_dir}" ]]; then | 
| +    # patch_dir exists, but is not a directory - what's that mean? | 
| +    note "patch_dir = ${patch_dir}" | 
| +    err "patch_dir must be a directory" | 
| exit 2 | 
| +  else | 
| +    # patch_dir does not exist - this is a full "installer." | 
| +    patch_dir= | 
| fi | 
| -  note "update_version_app = ${update_version_app}" | 
| +  note "patch_dir = ${patch_dir}" | 
| +  note "is_patch = ${is_patch}" | 
| +  note "dirpatcher = ${dirpatcher}" | 
|  | 
| -  local update_ks_plist="${update_app_plist}" | 
| -  note "update_ks_plist = ${update_ks_plist}" | 
| -  local update_version_ks | 
| -  if ! update_version_ks="$(defaults read "${update_ks_plist}" \ | 
| -                                          "${KS_VERSION_KEY}")" || | 
| -     [[ -z "${update_version_ks}" ]]; then | 
| -    err "couldn't determine update_version_ks" | 
| -    exit 2 | 
| -  fi | 
| -  note "update_version_ks = ${update_version_ks}" | 
| +  # The update to install. | 
|  | 
| -  local product_id | 
| -  if ! product_id="$(defaults read "${update_ks_plist}" \ | 
| -                                   "${KS_PRODUCT_KEY}")" || | 
| -     [[ -z "${product_id}" ]]; then | 
| -    err "couldn't determine product_id" | 
| -    exit 2 | 
| +  # update_app is the path to the new version of the .app.  It will only be | 
| +  # set at this point for a non-patch update.  It is not yet set for a patch | 
| +  # update because no such directory exists yet; it will be set later when | 
| +  # dirpatcher creates it. | 
| +  local update_app= | 
| + | 
| +  # update_version_app_old, patch_app_dir, and patch_versioned_dir will only | 
| +  # be set for patch updates. | 
| +  local update_version_app_old= | 
| +  local patch_app_dir= | 
| +  local patch_versioned_dir= | 
| + | 
| +  local update_version_app update_version_ks product_id | 
| +  if [[ -z "${is_patch}" ]]; then | 
| +    update_app="${update_dmg_mount_point}/${APP_DIR}" | 
| +    note "update_app = ${update_app}" | 
| + | 
| +    # Make sure that there's something to copy from, and that it's an absolute | 
| +    # path. | 
| +    if [[ "${update_app:0:1}" != "/" ]] || | 
| +       ! [[ -d "${update_app}" ]]; then | 
| +      err "update_app must be an absolute path to a directory" | 
| +      exit 2 | 
| +    fi | 
| + | 
| +    # Get some information about the update. | 
| +    note "reading update values" | 
| + | 
| +    local update_app_plist="${update_app}/${APP_PLIST}" | 
| +    note "update_app_plist = ${update_app_plist}" | 
| +    if ! update_version_app="$(defaults read "${update_app_plist}" \ | 
| +                                             "${APP_VERSION_KEY}")" || | 
| +       [[ -z "${update_version_app}" ]]; then | 
| +      err "couldn't determine update_version_app" | 
| +      exit 2 | 
| +    fi | 
| +    note "update_version_app = ${update_version_app}" | 
| + | 
| +    local update_ks_plist="${update_app_plist}" | 
| +    note "update_ks_plist = ${update_ks_plist}" | 
| +    if ! update_version_ks="$(defaults read "${update_ks_plist}" \ | 
| +                                            "${KS_VERSION_KEY}")" || | 
| +       [[ -z "${update_version_ks}" ]]; then | 
| +      err "couldn't determine update_version_ks" | 
| +      exit 2 | 
| +    fi | 
| +    note "update_version_ks = ${update_version_ks}" | 
| + | 
| +    if ! product_id="$(defaults read "${update_ks_plist}" \ | 
| +                                     "${KS_PRODUCT_KEY}")" || | 
| +       [[ -z "${product_id}" ]]; then | 
| +      err "couldn't determine product_id" | 
| +      exit 2 | 
| +    fi | 
| +    note "product_id = ${product_id}" | 
| +  else  # [[ -n "${is_patch}" ]] | 
| +    # Get some information about the update. | 
| +    note "reading update values" | 
| + | 
| +    if ! update_version_app_old=$(<"${patch_dir}/old_app_version") || | 
| +       [[ -z "${update_version_app_old}" ]]; then | 
| +      err "couldn't determine update_version_app_old" | 
| +      exit 2 | 
| +    fi | 
| +    note "update_version_app_old = ${update_version_app_old}" | 
| + | 
| +    if ! update_version_app=$(<"${patch_dir}/new_app_version") || | 
| +       [[ -z "${update_version_app}" ]]; then | 
| +      err "couldn't determine update_version_app" | 
| +      exit 2 | 
| +    fi | 
| +    note "update_version_app = ${update_version_app}" | 
| + | 
| +    if ! update_version_ks=$(<"${patch_dir}/new_ks_version") || | 
| +       [[ -z "${update_version_ks}" ]]; then | 
| +      err "couldn't determine update_version_ks" | 
| +      exit 2 | 
| +    fi | 
| +    note "update_version_ks = ${update_version_ks}" | 
| + | 
| +    if ! product_id=$(<"${patch_dir}/ks_product") || | 
| +       [[ -z "${product_id}" ]]; then | 
| +      err "couldn't determine product_id" | 
| +      exit 2 | 
| +    fi | 
| +    note "product_id = ${product_id}" | 
| + | 
| +    patch_app_dir="${patch_dir}/application.dirpatch" | 
| +    if ! [[ -d "${patch_app_dir}" ]]; then | 
| +      err "couldn't locate patch_app_dir" | 
| +      exit 6 | 
| +    fi | 
| +    note "patch_app_dir = ${patch_app_dir}" | 
| + | 
| +    patch_versioned_dir=\ | 
| +"${patch_dir}/version_${update_version_app_old}_${update_version_app}.dirpatch" | 
| +    if ! [[ -d "${patch_versioned_dir}" ]]; then | 
| +      err "couldn't locate patch_versioned_dir" | 
| +      exit 6 | 
| +    fi | 
| +    note "patch_versioned_dir = ${patch_versioned_dir}" | 
| fi | 
| -  note "product_id = ${product_id}" | 
|  | 
| # ksadmin is required. Keystone should have set a ${PATH} that includes it. | 
| # Check that here, so that more useful feedback can be offered in the | 
| @@ -479,6 +668,22 @@ | 
| "${APP_VERSION_KEY}" || true)" | 
| note "old_version_app = ${old_version_app}" | 
|  | 
| +  # old_version_app is not required, because it won't be present in skeleton | 
| +  # bootstrap installations, which just have an empty .app directory.  Only | 
| +  # require it when doing a patch update, and use it to validate that the | 
| +  # patch applies to the old installed version.  By definition, skeleton | 
| +  # bootstraps can't be installed with patch udpates.  They require the full | 
| +  # application on the disk image. | 
| +  if [[ -n "${is_patch}" ]]; then | 
| +    if [[ -z "${old_version_app}" ]]; then | 
| +      err "old_version_app required for patch" | 
| +      exit 6 | 
| +    elif [[ "${old_version_app}" != "${update_version_app_old}" ]]; then | 
| +      err "this patch does not apply to the installed version" | 
| +      exit 6 | 
| +    fi | 
| +  fi | 
| + | 
| local installed_versions_dir="${installed_app}/${VERSIONS_DIR}" | 
| note "installed_versions_dir = ${installed_versions_dir}" | 
|  | 
| @@ -500,119 +705,147 @@ | 
| true)" | 
| note "old_brand = ${old_brand}" | 
|  | 
| -  # See if the timestamp of what's currently on disk is newer than the | 
| -  # update's outer .app's timestamp.  rsync will copy the update's timestamp | 
| -  # over, but if that timestamp isn't as recent as what's already on disk, the | 
| -  # .app will need to be touched. | 
| -  local needs_touch= | 
| -  if [[ "${installed_app}" -nt "${update_app}" ]]; then | 
| -    needs_touch="y" | 
| -  fi | 
| -  note "needs_touch = ${needs_touch}" | 
| +  ensure_writable_symlinks_recursive "${installed_app}" | 
|  | 
| -  # In some very weird and rare cases, it is possible to wind up with a user | 
| -  # installation that contains symbolic links that the user does not have | 
| -  # write permission over.  More on how that might happen later. | 
| -  # | 
| -  # If a weird and rare case like this is observed, rsync will exit with an | 
| -  # error when attempting to update the times on these symbolic links.  rsync | 
| -  # may not be intelligent enough to try creating a new symbolic link in these | 
| -  # cases, but this script can be. | 
| -  # | 
| -  # This fix-up is not necessary when running as root, because root will | 
| -  # always be able to write everything needed. | 
| -  # | 
| -  # The problem occurs when an administrative user first drag-installs the | 
| -  # application to /Applications, resulting in the program's user being set to | 
| -  # the user's own ID.  If, subsequently, a .pkg package is installed over | 
| -  # that, the existing directory ownership will be preserved, but file | 
| -  # ownership will be changed to whateer is specified by the package, | 
| -  # typically root.  This applies to symbolic links as well.  On a subsequent | 
| -  # update, rsync will be able to copy the new files into place, because the | 
| -  # user still has permission to write to the directories.  If the symbolic | 
| -  # link targets are not changing, though, rsync will not replace them, and | 
| -  # they will remain owned by root.  The user will not have permission to | 
| -  # update the time on the symbolic links, resulting in an rsync error. | 
| -  if [[ ${EUID} -ne 0 ]]; then | 
| -    # This step isn't critical. | 
| -    set +e | 
| -    note "fixing installed symbolic links" | 
| - | 
| -    # Only consider symbolic links in ${update_app}.  If there are any other | 
| -    # links in ${installed_app} not present in ${update_app}, rsync will | 
| -    # delete them as needed later.  Use find -print0 with read -d $'\0' to | 
| -    # handle even the weirdest paths. | 
| -    local update_link | 
| -    while IFS= read -r -d $'\0' update_link; do | 
| -      # ${update_link} is relative to ${update_app}.  Prepending | 
| -      # ${installed_app} looks for the same link already on disk. | 
| -      local installed_link="${installed_app}/${update_link}" | 
| -      note "ensure_writable_symlink ${installed_link}" | 
| -      ensure_writable_symlink "${installed_link}" | 
| -    done < <(cd "${update_app}" && find . -type l -print0) | 
| - | 
| -    # Go back to how things were. | 
| -    set -e | 
| -  fi | 
| - | 
| # By copying to ${installed_app}, the existing application name will be | 
| # preserved, if the user has renamed the application on disk.  Respecting | 
| # the user's changes is friendly. | 
|  | 
| # Make sure that ${installed_versions_dir} exists, so that it can receive | 
| # the versioned directory.  It may not exist if updating from an older | 
| -  # version that did not use the versioned layout on disk.  An rsync that | 
| -  # excludes all contents is used to bring the permissions over from | 
| -  # ${update_versions_dir}, otherwise, this directory would be the only one in | 
| -  # the entire update exempt from getting its permissions copied over.  A | 
| -  # simple mkdir wouldn't copy mode bits.  This is done even if | 
| -  # ${installed_versions_dir} already does exist to ensure that the mode bits | 
| -  # come from the update. | 
| +  # version that did not use the versioned layout on disk.  Later, during the | 
| +  # rsync to copy the applciation directory, the mode bits and timestamp on | 
| +  # ${installed_versions_dir} will be set to conform to whatever is present in | 
| +  # the update. | 
| # | 
| # ${installed_app} is guaranteed to exist at this point, but | 
| # ${installed_app}/${CONTENTS_DIR} may not if things are severely broken or | 
| # if this update is actually an initial installation from a Keystone | 
| # skeleton bootstrap.  The mkdir creates ${installed_app}/${CONTENTS_DIR} if | 
| # it doesn't exist; its mode bits will be fixed up in a subsequent rsync. | 
| -  note "creating CONTENTS_DIR" | 
| -  if ! mkdir -p "${installed_app}/${CONTENTS_DIR}"; then | 
| -    err "mkdir of CONTENTS_DIR failed" | 
| +  note "creating installed_versions_dir" | 
| +  if ! mkdir -p "${installed_versions_dir}"; then | 
| +    err "mkdir of installed_versions_dir failed" | 
| exit 5 | 
| fi | 
|  | 
| -  local update_versions_dir="${update_app}/${VERSIONS_DIR}" | 
| -  note "update_versions_dir = ${update_versions_dir}" | 
| +  local new_versioned_dir | 
| +  new_versioned_dir="${installed_versions_dir}/${update_version_app}" | 
| +  note "new_versioned_dir = ${new_versioned_dir}" | 
|  | 
| -  note "rsyncing VERSIONS_DIR" | 
| -  if ! rsync ${RSYNC_FLAGS} --exclude "*" "${update_versions_dir}/" \ | 
| -                                          "${installed_versions_dir}"; then | 
| -    err "rsync of VERSIONS_DIR failed, status ${PIPESTATUS[0]}" | 
| -    exit 6 | 
| +  # If there's an entry at ${new_versioned_dir} but it's not a directory | 
| +  # (or it's a symbolic link, whether or not it points to a directory), rsync | 
| +  # won't get rid of it.  It's never correct to have a non-directory in place | 
| +  # of the versioned directory, so toss out whatever's there.  Don't treat | 
| +  # this as a critical step: if removal fails, operation can still proceed to | 
| +  # to the dirpatcher or rsync, which will likely fail. | 
| +  if [[ -e "${new_versioned_dir}" ]] && | 
| +     ([[ -L "${new_versioned_dir}" ]] || | 
| +      ! [[ -d "${new_versioned_dir}" ]]); then | 
| +    note "removing non-directory in place of versioned directory" | 
| +    rm -f "${new_versioned_dir}" 2> /dev/null || true | 
| fi | 
|  | 
| +  local update_versioned_dir | 
| +  if [[ -z "${is_patch}" ]]; then | 
| +    update_versioned_dir="${update_app}/${VERSIONS_DIR}/${update_version_app}" | 
| +    note "update_versioned_dir = ${update_versioned_dir}" | 
| +  else  # [[ -n "${is_patch}" ]] | 
| +    # dirpatcher won't patch into a directory that already exists.  Doing so | 
| +    # would be a bad idea, anyway.  If ${new_versioned_dir} already exists, | 
| +    # it may be something left over from a previous failed or incomplete | 
| +    # update attempt, or it may be the live versioned directory if this is a | 
| +    # same-version update intended only to change channels.  Since there's no | 
| +    # way to tell, this case is handled by having dirpatcher produce the new | 
| +    # versioned directory in a temporary location and then having rsync copy | 
| +    # it into place as an ${update_versioned_dir}, the same as in a non-patch | 
| +    # update.  If ${new_versioned_dir} doesn't exist, dirpatcher can place the | 
| +    # new versioned directory at that location directly. | 
| +    local versioned_dir_target | 
| +    if ! [[ -e "${new_versioned_dir}" ]]; then | 
| +      versioned_dir_target="${new_versioned_dir}" | 
| +      note "versioned_dir_target = ${versioned_dir_target}" | 
| +    else | 
| +      ensure_temp_dir | 
| +      versioned_dir_target="${g_temp_dir}/${update_version_app}" | 
| +      note "versioned_dir_target = ${versioned_dir_target}" | 
| +      update_versioned_dir="${versioned_dir_target}" | 
| +      note "update_versioned_dir = ${update_versioned_dir}" | 
| +    fi | 
| + | 
| +    note "dirpatching versioned directory" | 
| +    if ! "${dirpatcher}" "${old_versioned_dir}" \ | 
| +                         "${patch_versioned_dir}" \ | 
| +                         "${versioned_dir_target}"; then | 
| +      err "dirpatcher of versioned directory failed, status ${PIPESTATUS[0]}" | 
| +      exit 12 | 
| +    fi | 
| +  fi | 
| + | 
| # Copy the versioned directory.  The new versioned directory should have a | 
| # different name than any existing one, so this won't harm anything already | 
| # present in ${installed_versions_dir}, including the versioned directory | 
| # being used by any running processes.  If this step is interrupted, there | 
| # will be an incomplete versioned directory left behind, but it won't | 
| # won't interfere with anything, and it will be replaced or removed during a | 
| -  # future update attempt.  Note that in certain cases, same-version updates | 
| -  # are distributed to move users between channels; when this happens, the | 
| -  # contents of the versioned directories are identical and rsync will not | 
| -  # render the versioned directory unusable even for an instant. | 
| -  local update_versioned_dir new_versioned_dir | 
| -  update_versioned_dir="${update_versions_dir}/${update_version_app}" | 
| -  note "update_versioned_dir = ${update_versioned_dir}" | 
| -  new_versioned_dir="${installed_versions_dir}/${update_version_app}" | 
| -  note "new_versioned_dir = ${new_versioned_dir}" | 
| +  # future update attempt. | 
| +  # | 
| +  # In certain cases, same-version updates are distributed to move users | 
| +  # between channels; when this happens, the contents of the versioned | 
| +  # directories are identical and rsync will not render the versioned | 
| +  # directory unusable even for an instant. | 
| +  # | 
| +  # ${update_versioned_dir} may be empty during a patch update (${is_patch}) | 
| +  # if the dirpatcher above was able to write it into place directly.  In | 
| +  # that event, dirpatcher guarantees that ${new_versioned_dir} is already in | 
| +  # place. | 
| +  if [[ -n "${update_versioned_dir}" ]]; then | 
| +    note "rsyncing versioned directory" | 
| +    if ! rsync ${RSYNC_FLAGS} --delete-before "${update_versioned_dir}/" \ | 
| +                                              "${new_versioned_dir}"; then | 
| +      err "rsync of versioned directory failed, status ${PIPESTATUS[0]}" | 
| +      exit 7 | 
| +    fi | 
| +  fi | 
|  | 
| -  note "rsyncing versioned directory" | 
| -  if ! rsync ${RSYNC_FLAGS} --delete-before "${update_versioned_dir}/" \ | 
| -                                            "${new_versioned_dir}"; then | 
| -    err "rsync of versioned directory failed, status ${PIPESTATUS[0]}" | 
| -    exit 7 | 
| +  if [[ -n "${is_patch}" ]]; then | 
| +    # If the versioned directory was prepared in a temporary directory and | 
| +    # then rsynced into place, remove the temporary copy now that it's no | 
| +    # longer needed. | 
| +    if [[ -n "${update_versioned_dir}" ]]; then | 
| +      rm -rf "${update_versioned_dir}" 2> /dev/null || true | 
| +      update_versioned_dir= | 
| +      note "update_versioned_dir = ${update_versioned_dir}" | 
| +    fi | 
| + | 
| +    # Prepare ${update_app}.  This always needs to be done in a temporary | 
| +    # location because dirpatcher won't write to a directory that already | 
| +    # exists, and ${installed_app} needs to be used as input to dirpatcher | 
| +    # in any event.  The new application will be rsynced into place once | 
| +    # dirpatcher creates it. | 
| +    ensure_temp_dir | 
| +    update_app="${g_temp_dir}/${APP_DIR}" | 
| +    note "update_app = ${update_app}" | 
| + | 
| +    note "dirpatching app directory" | 
| +    if ! "${dirpatcher}" "${installed_app}" \ | 
| +                         "${patch_app_dir}" \ | 
| +                         "${update_app}"; then | 
| +      err "dirpatcher of app directory failed, status ${PIPESTATUS[0]}" | 
| +      exit 13 | 
| +    fi | 
| fi | 
|  | 
| +  # See if the timestamp of what's currently on disk is newer than the | 
| +  # update's outer .app's timestamp.  rsync will copy the update's timestamp | 
| +  # over, but if that timestamp isn't as recent as what's already on disk, the | 
| +  # .app will need to be touched. | 
| +  local needs_touch= | 
| +  if [[ "${installed_app}" -nt "${update_app}" ]]; then | 
| +    needs_touch="y" | 
| +  fi | 
| +  note "needs_touch = ${needs_touch}" | 
| + | 
| # Copy the unversioned files into place, leaving everything in | 
| # ${installed_versions_dir} alone.  If this step is interrupted, the | 
| # application will at least remain in a usable state, although it may not | 
| @@ -621,9 +854,11 @@ | 
| # critical point is when the main executable is replaced.  There isn't very | 
| # much to copy in this step, because most of the application is in the | 
| # versioned directory.  This step only accounts for around 50 files, most of | 
| -  # which are small localized InfoPlist.strings files. | 
| +  # which are small localized InfoPlist.strings files.  Note that | 
| +  # ${VERSIONS_DIR} is included to copy its mode bits and timestamp, but its | 
| +  # contents are excluded, having already been installed above. | 
| note "rsyncing app directory" | 
| -  if ! rsync ${RSYNC_FLAGS} --delete-after --exclude "/${VERSIONS_DIR}" \ | 
| +  if ! rsync ${RSYNC_FLAGS} --delete-after --exclude "/${VERSIONS_DIR}/*" \ | 
| "${update_app}/" "${installed_app}"; then | 
| err "rsync of app directory failed, status ${PIPESTATUS[0]}" | 
| exit 8 | 
| @@ -631,6 +866,20 @@ | 
|  | 
| note "rsyncs complete" | 
|  | 
| +  if [[ -n "${is_patch}" ]]; then | 
| +    # update_app has been rsynced into place and is no longer needed. | 
| +    rm -rf "${update_app}" 2> /dev/null || true | 
| +    update_app= | 
| +    note "update_app = ${update_app}" | 
| +  fi | 
| + | 
| +  if [[ -n "${g_temp_dir}" ]]; then | 
| +    # The temporary directory, if any, is no longer needed. | 
| +    rm -rf "${g_temp_dir}" 2> /dev/null || true | 
| +    g_temp_dir= | 
| +    note "g_temp_dir = ${g_temp_dir}" | 
| +  fi | 
| + | 
| # If necessary, touch the outermost .app so that it appears to the outside | 
| # world that something was done to the bundle.  This will cause | 
| # LaunchServices to invalidate the information it has cached about the | 
| @@ -967,6 +1216,9 @@ | 
|  | 
| # Great success! | 
| note "done!" | 
| + | 
| +  trap - EXIT | 
| + | 
| return 0 | 
| } | 
|  | 
|  |