Posted on 07-03-2018 09:53 AM
Unlike on Windows, if you've enabled Google Chrome for Mac to autoupdate for all users, over time you'll start to see an accumulation of old versions:
$ pwd
/Applications/Google Chrome.app/Contents/Versions
$ ls
65.0.3325.181 66.0.3359.170 67.0.3396.87 67.0.3396.99
$
(Again, these only appear if you've configured Google Chrome to autoupdate.)
Despite extensive searching online (Chrome's online documentation, Chrome's source code on chromium.org), I could not find any built-in mechanism in Google Chrome for Mac to clean out these old versions.
I did find a Google build tool (clean_up_old_versions.py), and modified it for my needs, while also borrowing some code from Hannes Juutilainen's excellent chrome-enable-autoupdates.py.
The resulting script now correctly detects the current version installed on disk, leaves it intact, and deletes all other versions installed inside the /Applications/Google Chrome.app/Contents/Versions folder:
$ sudo /tmp/clean_up_old_chrome_versions.py
Password:
Removing /Applications/Google Chrome.app/Contents/Versions/65.0.3325.181
Removing /Applications/Google Chrome.app/Contents/Versions/66.0.3359.170
Removing /Applications/Google Chrome.app/Contents/Versions/67.0.3396.87
$ pwd
/Applications/Google Chrome.app/Contents/Versions
$ ls
67.0.3396.99
$
The downside to having a separate script is that it isn't run automatically after Keystone updates Google Chrome, i.e. the script has to be run separately and periodically via a separate JAMF Pro policy.
Also, some enterprise sites may have other Google products installed (e.g. Google Earth) that are also under Keystone management, and modifying this script (or creating yet another version of it) soon becomes an unwieldy approach.
So... my questions:
Ideally, Google would add support to Google Chrome for Mac (and other Google software for Mac) to clean old versions automatically, immediately after each update is installed. A feature request may need to be submitted to Google.
Posted on 07-04-2018 10:25 PM
we created an EA that records the number of versions (i.e. count the folders) then have a policy that simply deletes the entire folder and reinstall Chrome direct from Google. I think it's based on this script.
Posted on 07-05-2018 01:45 PM
Could you post your script on here? We're seeing the same issue occur over here.
Posted on 07-05-2018 06:04 PM
@echalupka Certainly. Here you go:
#!/usr/bin/env python
# clean_chrome_versions.py
#
# Manavendra Thakur
# International Monetary Fund
# mthakur@imf.org
#
# v1.0
# 2018-07-05
# ChangeLog
# v1.0 2018-07-05
# Initial version
# Credits
# Derived from
# https://cs.chromium.org/chromium/src/chrome/tools/build/mac/clean_up_old_versions.py
# Retained copyright and license notice:
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
# Code portions borrowed from:
# chrome-enable-autoupdates.py
# https://github.com/hjuutilainen/adminscripts
# Created by Hannes Juutilainen, hjuutilainen@mac.com
import optparse
import os
import shutil
import sys
import plistlib
import pwd
import stat
default_chrome_path = "/Applications/Google Chrome.app"
default_umask = 0o022
def clean_app_versions(path_to_app, app_keep_version):
"""Delete all Google Chrome versions except keep version"""
versions_dir = os.path.join(path_to_app, 'Contents', 'Versions')
if os.path.exists(versions_dir):
for version in os.listdir(versions_dir):
if version != app_keep_version:
path_to_delete = os.path.join(versions_dir, version)
print "Removing %s" % (path_to_delete)
shutil.rmtree(path_to_delete)
else:
print >> sys.stderr, "Error: Path %s does not exist." % (versions_dir)
return 1
def chrome_installed(chrome_path):
"""Check if Chrome is installed"""
if os.path.exists(chrome_path):
return True
else:
return False
def installed_chrome_version(chrome_path):
"""Returns Chrome version"""
info_plist_path = os.path.realpath(os.path.join(chrome_path, 'Contents/Info.plist'))
info_plist = plistlib.readPlist(info_plist_path)
bundle_short_version = info_plist["CFBundleShortVersionString"]
return bundle_short_version
def clean_user_level_keystone_updates(disable_current_updates, block_future_updates, umask):
"""Disables Google Keystone in user home folders"""
# For each user on computer...
for p in pwd.getpwall():
user_id = p.pw_uid
group_id = p.pw_gid
user_home_dir = p.pw_dir
# Exclude system accounts
if user_home_dir.startswith("/var/") or user_home_dir.startswith("/private/") or user_home_dir.startswith("/Library/"):
continue
google_dir = os.path.join(user_home_dir, "Library/Google")
googlesoftwareupdate_dir = os.path.join(google_dir, "GoogleSoftwareUpdate")
# If ~/Library/Google/GoogleSoftwareUpdate exists...
if os.path.exists(googlesoftwareupdate_dir):
flags = os.stat(googlesoftwareupdate_dir).st_flags
# Make no changes if folder is locked, even though sudo user can override
mutable = not bool(flags & stat.UF_IMMUTABLE)
if mutable:
if disable_current_updates:
# Delete contents of ~/Library/Google/GoogleSoftwareUpdate
for filename in os.listdir(googlesoftwareupdate_dir):
filepath = os.path.join(googlesoftwareupdate_dir, filename)
try:
shutil.rmtree(filepath)
except OSError:
os.remove(filepath)
if block_future_updates:
# Lock folder to prevent future Keystone update attempts
os.chflags(googlesoftwareupdate_dir, flags | stat.UF_IMMUTABLE)
else:
# ~/Library/Google/GoogleSoftwareUpdate does not exist
if block_future_updates:
# Create ~/Library/Google/GoogleSoftwareUpdate
old_umask = os.umask(umask)
if os.path.exists(google_dir):
os.mkdir(googlesoftwareupdate_dir)
os.chown(googlesoftwareupdate_dir, user_id, group_id)
else:
os.makedirs(googlesoftwareupdate_dir)
os.chown(google_dir, user_id, group_id)
os.chown(googlesoftwareupdate_dir, user_id, group_id)
# Lock folder to prevent future Keystone update attempts
flags = os.stat(googlesoftwareupdate_dir).st_flags
os.chflags(googlesoftwareupdate_dir, flags | stat.UF_IMMUTABLE)
os.umask(old_umask)
def Main():
"""The main routine."""
Usage = """%prog [options] [Chrome path] [keep version]
Deletes non-current (both older and newer) versions of Google
Chrome that can accumulate in the /Applications/Google
Chrome.app/Contents/Versions folder over time if Google Chrome
auto-update is enabled for all users.
Optional Arguments: Specify path to Google Chrome app and
version to keep intact.
Defaults to "/Applications/Google Chrome.app" and installed
version.
This script must be run with admin privilegs.
Examples:
Clean Chrome versions from "/Applications/Google
Chrome.app/Contents/Versions" folder:
sudo clean_chrome_versions.py
Same as above, and delete current Keystone updates, and block
future Keystone updates:
sudo clean_chrome_versions.py -a
Same as above, but with umask of 077 (i.e. "drwx------"
permissions on any created folders):
sudo clean_chrome_versions.py -a -u 077
Specify nonstandard path and specific version to keep:
sudo clean_chrome_versions.py "/Path/To/Google Chrome.app" 1.2.3.4
"""
parser = optparse.OptionParser(usage=Usage)
parser.add_option('-a', '--all', action='store_true', help=("Disable both current and future Google Keystone updates for existing users, except system accounts. Same as specifying -d and -b."))
parser.add_option('-d', '--disable-current', action='store_true', help=("Disable current Google Keystone updates for existing users, except system accounts, by deleting contents of ~/Library/Google/GoogleSoftwareUpdate."))
parser.add_option('-b', '--block-future', action='store_true', help=(
"Block future Google Keystone updates for existing users, except system accounts, by locking the ~/Library/Google/GoogleSoftwareUpdate folder, creating this folder if it doesn't exist."))
parser.add_option('-u', '--umask', type="int", help=(
"Umask used while creating Keystone update folders. Defaults to %s." % oct(default_umask)))
opts, args = parser.parse_args()
if len(args) != 2:
chrome_path = default_chrome_path
keep_version = installed_chrome_version(chrome_path)
else:
chrome_path = args[0]
keep_version = args[1]
all = opts.all or False
disable_current = opts.disable_current or all or False
block_future = opts.block_future or all or False
umask = opts.umask or default_umask
# Check for root
if os.geteuid() != 0:
print >> sys.stderr, "Error: This script must be run as root"
return 1
if not chrome_installed(chrome_path):
print >> sys.stderr, "Error: Chrome path %s does not exist." % (chrome_path)
return 1
clean_app_versions(chrome_path, keep_version)
if disable_current or block_future:
clean_user_level_keystone_updates(disable_current, block_future, umask)
return 0
if __name__ == '__main__':
sys.exit(Main())
Posted on 07-05-2018 06:07 PM
@echalupka Please invoke the code from the command line as follows:
Clean Chrome versions from "/Applications/Google Chrome.app/Contents/Versions" folder:$ sudo clean_chrome_versions.py
Same as above, and delete current Keystone updates, and block future Keystone updates:$ sudo clean_chrome_versions.py -a
Same as above, but with umask of 077 (i.e. "drwx------" permissions on any created folders):$ sudo clean_chrome_versions.py -a -u 077
Specify nonstandard path and specific version to keep:$ sudo clean_chrome_versions.py "/Path/To/Google Chrome.app" 1.2.3.4
Posted on 07-15-2018 11:47 AM
Thanks a bunch @mthakur !
08-24-2021 09:59 AM - edited 08-24-2021 10:01 AM
I had to make a few changes to get this working on macOS Big Sur 11.5.x with modern versions of Chrome (test with 91.x+).
diff --git a/clean_chrome_versions.py b/clean_chrome_versions.py
index f23533d..9d5fc86 100755
--- a/clean_chrome_versions.py
+++ b/clean_chrome_versions.py
@@ -41,15 +41,19 @@ default_umask = 0o022
def clean_app_versions(path_to_app, app_keep_version):
"""Delete all Google Chrome versions except keep version"""
- versions_dir = os.path.join(path_to_app, 'Contents', 'Versions')
+ new_versions_path = '/Applications/Google Chrome.app/Contents/Frameworks/Google Chrome Framework.framework/Versions/'
+ versions_dir = os.path.join(new_versions_path)
if os.path.exists(versions_dir):
+ # Detect which version Current symlink points to and don't try to rm Current or that version
for version in os.listdir(versions_dir):
- if version != app_keep_version:
+ if version == 'Current':
+ next
+ elif version != app_keep_version:
path_to_delete = os.path.join(versions_dir, version)
- print "Removing %s" % (path_to_delete)
+ print("Removing %s" % (path_to_delete))
shutil.rmtree(path_to_delete)
else:
- print >> sys.stderr, "Error: Path %s does not exist." % (versions_dir)
+ sys.stderr.write("Error: Path %s does not exist." % (versions_dir))
return 1
def chrome_installed(chrome_path):
@@ -62,9 +66,10 @@ def chrome_installed(chrome_path):
def installed_chrome_version(chrome_path):
"""Returns Chrome version"""
info_plist_path = os.path.realpath(os.path.join(chrome_path, 'Contents/Info.plist'))
- info_plist = plistlib.readPlist(info_plist_path)
- bundle_short_version = info_plist["CFBundleShortVersionString"]
- return bundle_short_version
+ with open(info_plist_path, 'rb') as plist:
+ info_plist = plistlib.load(plist)
+ bundle_short_version = info_plist["CFBundleShortVersionString"]
+ return bundle_short_version
def clean_user_level_keystone_updates(disable_current_updates, block_future_updates, umask):
"""Disables Google Keystone in user home folders"""
@@ -168,11 +173,11 @@ def Main():
# Check for root
if os.geteuid() != 0:
- print >> sys.stderr, "Error: This script must be run as root"
+ sys.stderr.write("Error: This script must be run as root")
return 1
if not chrome_installed(chrome_path):
- print >> sys.stderr, "Error: Chrome path %s does not exist." % (chrome_path)
+ sys.stderr.write("Error: Chrome path %s does not exist." % (chrome_path))
return 1
clean_app_versions(chrome_path, keep_version)
Full updated version:
#!/usr/bin/env python
# clean_chrome_versions.py
#
# Manavendra Thakur
# International Monetary Fund
# mthakur@imf.org
#
# v1.0
# 2018-07-05
# ChangeLog
# v1.0 2018-07-05
# Initial version
# Credits
# Derived from
# https://cs.chromium.org/chromium/src/chrome/tools/build/mac/clean_up_old_versions.py
# Retained copyright and license notice:
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
# Code portions borrowed from:
# chrome-enable-autoupdates.py
# https://github.com/hjuutilainen/adminscripts
# Created by Hannes Juutilainen, hjuutilainen@mac.com
import optparse
import os
import shutil
import sys
import plistlib
import pwd
import stat
default_chrome_path = "/Applications/Google Chrome.app"
default_umask = 0o022
def clean_app_versions(path_to_app, app_keep_version):
"""Delete all Google Chrome versions except keep version"""
new_versions_path = '/Applications/Google Chrome.app/Contents/Frameworks/Google Chrome Framework.framework/Versions/'
versions_dir = os.path.join(path_to_app, new_versions_path)
if os.path.exists(versions_dir):
# Detect which version Current symlink points to and don't try to rm Current or that version
for version in os.listdir(versions_dir):
if version == 'Current':
next
elif version != app_keep_version:
path_to_delete = os.path.join(versions_dir, version)
print("Removing %s" % (path_to_delete))
shutil.rmtree(path_to_delete)
else:
sys.stderr.write("Error: Path %s does not exist." % (versions_dir))
return 1
def chrome_installed(chrome_path):
"""Check if Chrome is installed"""
if os.path.exists(chrome_path):
return True
else:
return False
def installed_chrome_version(chrome_path):
"""Returns Chrome version"""
info_plist_path = os.path.realpath(os.path.join(chrome_path, 'Contents/Info.plist'))
with open(info_plist_path, 'rb') as plist:
info_plist = plistlib.load(plist)
bundle_short_version = info_plist["CFBundleShortVersionString"]
return bundle_short_version
def clean_user_level_keystone_updates(disable_current_updates, block_future_updates, umask):
"""Disables Google Keystone in user home folders"""
# For each user on computer...
for p in pwd.getpwall():
user_id = p.pw_uid
group_id = p.pw_gid
user_home_dir = p.pw_dir
# Exclude system accounts
if user_home_dir.startswith("/var/") or user_home_dir.startswith("/private/") or user_home_dir.startswith("/Library/"):
continue
google_dir = os.path.join(user_home_dir, "Library/Google")
googlesoftwareupdate_dir = os.path.join(google_dir, "GoogleSoftwareUpdate")
# If ~/Library/Google/GoogleSoftwareUpdate exists...
if os.path.exists(googlesoftwareupdate_dir):
flags = os.stat(googlesoftwareupdate_dir).st_flags
# Make no changes if folder is locked, even though sudo user can override
mutable = not bool(flags & stat.UF_IMMUTABLE)
if mutable:
if disable_current_updates:
# Delete contents of ~/Library/Google/GoogleSoftwareUpdate
for filename in os.listdir(googlesoftwareupdate_dir):
filepath = os.path.join(googlesoftwareupdate_dir, filename)
try:
shutil.rmtree(filepath)
except OSError:
os.remove(filepath)
if block_future_updates:
# Lock folder to prevent future Keystone update attempts
os.chflags(googlesoftwareupdate_dir, flags | stat.UF_IMMUTABLE)
else:
# ~/Library/Google/GoogleSoftwareUpdate does not exist
if block_future_updates:
# Create ~/Library/Google/GoogleSoftwareUpdate
old_umask = os.umask(umask)
if os.path.exists(google_dir):
os.mkdir(googlesoftwareupdate_dir)
os.chown(googlesoftwareupdate_dir, user_id, group_id)
else:
os.makedirs(googlesoftwareupdate_dir)
os.chown(google_dir, user_id, group_id)
os.chown(googlesoftwareupdate_dir, user_id, group_id)
# Lock folder to prevent future Keystone update attempts
flags = os.stat(googlesoftwareupdate_dir).st_flags
os.chflags(googlesoftwareupdate_dir, flags | stat.UF_IMMUTABLE)
os.umask(old_umask)
def Main():
"""The main routine."""
Usage = """%prog [options] [Chrome path] [keep version]
Deletes non-current (both older and newer) versions of Google
Chrome that can accumulate in the /Applications/Google
Chrome.app/Contents/Versions folder over time if Google Chrome
auto-update is enabled for all users.
Optional Arguments: Specify path to Google Chrome app and
version to keep intact.
Defaults to "/Applications/Google Chrome.app" and installed
version.
This script must be run with admin privilegs.
Examples:
Clean Chrome versions from "/Applications/Google
Chrome.app/Contents/Versions" folder:
sudo clean_chrome_versions.py
Same as above, and delete current Keystone updates, and block
future Keystone updates:
sudo clean_chrome_versions.py -a
Same as above, but with umask of 077 (i.e. "drwx------"
permissions on any created folders):
sudo clean_chrome_versions.py -a -u 077
Specify nonstandard path and specific version to keep:
sudo clean_chrome_versions.py "/Path/To/Google Chrome.app" 1.2.3.4
"""
parser = optparse.OptionParser(usage=Usage)
parser.add_option('-a', '--all', action='store_true', help=("Disable both current and future Google Keystone updates for existing users, except system accounts. Same as specifying -d and -b."))
parser.add_option('-d', '--disable-current', action='store_true', help=("Disable current Google Keystone updates for existing users, except system accounts, by deleting contents of ~/Library/Google/GoogleSoftwareUpdate."))
parser.add_option('-b', '--block-future', action='store_true', help=(
"Block future Google Keystone updates for existing users, except system accounts, by locking the ~/Library/Google/GoogleSoftwareUpdate folder, creating this folder if it doesn't exist."))
parser.add_option('-u', '--umask', type="int", help=(
"Umask used while creating Keystone update folders. Defaults to %s." % oct(default_umask)))
opts, args = parser.parse_args()
if len(args) != 2:
chrome_path = default_chrome_path
keep_version = installed_chrome_version(chrome_path)
else:
chrome_path = args[0]
keep_version = args[1]
all = opts.all or False
disable_current = opts.disable_current or all or False
block_future = opts.block_future or all or False
umask = opts.umask or default_umask
# Check for root
if os.geteuid() != 0:
sys.stderr.write("Error: This script must be run as root")
return 1
if not chrome_installed(chrome_path):
sys.stderr.write("Error: Chrome path %s does not exist." % (chrome_path))
return 1
clean_app_versions(chrome_path, keep_version)
if disable_current or block_future:
clean_user_level_keystone_updates(disable_current, block_future, umask)
return 0
if __name__ == '__main__':
sys.exit(Main())
08-24-2021 10:06 AM - edited 08-25-2021 11:01 AM
Silly reply submission making me do a duplicate post since the first one didn't show up, and then the second didn't either for quite some time.