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.
Could you post your script on here? We're seeing the same issue occur over here.
@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())
@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
Thanks a bunch @mthakur !
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())
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.