From 14156218b0013d670a82783e33b14e5a15d1b252 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sun, 25 Jan 2015 15:32:14 -0800 Subject: [PATCH] swc-installation-test-2.py: Convert PathCommandDependency to VersionPlistCommandDependency Philip Guo suggested using version.plist to lookup the version number for applications on OS X [1]. Searching around, I found an example of Safari's version.plist posted by ASmit010 [2]: BuildVersion 1 CFBundleShortVersionString 5.1.7 CFBundleVersion 7534.57.2 ProjectName WebBrowser SourceVersion 7534057002000000 Looking at that example, the version information we want is associated with CFBundleShortVersionString [3]: CFBundleShortVersionString (String - iOS, OS X) specifies the release version number of the bundle, which identifies a released iteration of the app. The release version number is a string comprised of three period-separated integers. The first integer represents major revisions to the app, such as revisions that implement new features or major changes. The second integer denotes revisions that implement less prominent features. The third integer represents maintenance releases. The value for this key differs from the value for CFBundleVersion, which identifies an iteration (released or unreleased) of the app. This key can be localized by including it in your InfoPlist.strings files The sibling-entries for keys and values are a bit awkward using Python's stock ElementTree [4,5], which doesn't have built-in methods for finding parents or siblings [6]. I've followed the example set by lxml and added _get_parent and _get_next helpers (mirroring getparent [7] and getnext [8]) to make it the logic in _get_version_from_plist more clear. [1]: https://twitter.com/pgbovine/status/559094439009075203 [2]: https://discussions.apple.com/message/19757845#19757845 [3]: https://developer.apple.com/library/ios/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html#//apple_ref/doc/uid/20001431-111349 [4]: https://docs.python.org/2/library/xml.etree.elementtree.html [5]: https://docs.python.org/3/library/xml.etree.elementtree.html [6]: http://lxml.de/1.3/api.html#trees-and-documents [7]: http://lxml.de/api/lxml.etree._Element-class.html#getparent [8]: http://lxml.de/api/lxml.etree._Element-class.html#getnext --- swc-installation-test-2.py | 77 +++++++++++++++++++++++++++++--------- 1 file changed, 60 insertions(+), 17 deletions(-) diff --git a/swc-installation-test-2.py b/swc-installation-test-2.py index 16532ff..54e5250 100755 --- a/swc-installation-test-2.py +++ b/swc-installation-test-2.py @@ -50,6 +50,7 @@ try: # Python 3.x import urllib.parse as _urllib_parse except ImportError: # Python 2.x import urllib as _urllib_parse # for quote() +import xml.etree.ElementTree as _element_tree if not hasattr(_shlex, 'quote'): # Python versions older than 3.3 @@ -477,26 +478,62 @@ class CommandDependency (Dependency): return match.group(1) -class PathCommandDependency (CommandDependency): +class VersionPlistCommandDependency (CommandDependency): """A command that doesn't support --version or equivalent options - On some operating systems (e.g. OS X), a command's executable may - be hard to find, or not exist in the PATH. Work around that by - just checking for the existence of a characteristic file or - directory. Since the characteristic path may depend on OS, - installed version, etc., take a list of paths, and succeed if any - of them exists. + On OS X, a command's executable may be hard to find, or not exist + in the PATH. Work around that by looking up the version + information in the package's version.plist file. """ + def __init__(self, key='CFBundleShortVersionString', **kwargs): + super(VersionPlistCommandDependency, self).__init__(**kwargs) + self.key = key + def _get_command_version_stream(self, *args, **kwargs): raise NotImplementedError() def _get_version_stream(self, *args, **kwargs): raise NotImplementedError() + @staticmethod + def _get_parent(root, element): + """Returns the parent of this element or None for the root element + """ + for node in root.iter(): + if element in node: + return node + raise ValueError((root, element)) + + @staticmethod + def _get_next(root, element): + """Returns the following sibling of this element or None + """ + parent = self._get_parent(root=root, element=element) + siblings = iter(parent) + for node in siblings: + if node == element: + try: + return next(siblings) + except StopIteration: + return None + return None + + def _get_version_from_plist(self, path): + """Parse the plist and return the value string for self.key + """ + tree = _element_tree.parse(source=path) + data = {} + for key in tree.findall('.//key'): + value = self._get_next(root=tree, element=key) + if value.tag != 'string': + raise ValueError((tree, key, value)) + data[key.text] = value.text + return data[self.key] + def _get_version(self): for path in self.paths: if _os.path.exists(path): - return None + return self._get_version_from_plist(path=path) raise DependencyError( checker=self, message=( @@ -746,23 +783,28 @@ CHECKER['py.test'] = CommandDependency( for paths,name,long_name in [ - ([_os.path.join(_ROOT_PATH, 'Applications', 'Sublime Text 2.app')], + ([_os.path.join(_ROOT_PATH, 'Applications', 'Sublime Text 2.app', + 'Contents', 'version.plist')], 'sublime-text', 'Sublime Text'), - ([_os.path.join(_ROOT_PATH, 'Applications', 'TextMate.app')], + ([_os.path.join(_ROOT_PATH, 'Applications', 'TextMate.app', + 'Contents', 'version.plist')], 'textmate', 'TextMate'), - ([_os.path.join(_ROOT_PATH, 'Applications', 'TextWrangler.app')], + ([_os.path.join(_ROOT_PATH, 'Applications', 'TextWrangler.app', + 'Contents', 'version.plist')], 'textwrangler', 'TextWrangler'), - ([_os.path.join(_ROOT_PATH, 'Applications', 'Safari.app')], + ([_os.path.join(_ROOT_PATH, 'Applications', 'Safari.app', + 'Contents', 'version.plist')], 'safari', 'Safari'), - ([_os.path.join(_ROOT_PATH, 'Applications', 'Xcode.app'), # OS X >=1.7 - _os.path.join(_ROOT_PATH, 'Developer', 'Applications', 'Xcode.app' - ) # OS X 1.6, + ([_os.path.join(_ROOT_PATH, 'Applications', 'Xcode.app', + 'Contents', 'version.plist'), # OS X >=1.7 + _os.path.join(_ROOT_PATH, 'Developer', 'Applications', 'Xcode.app', + 'Contents', 'version.plist'), # OS X 1.6, ], 'xcode', 'Xcode'), ]: if not long_name: long_name = name - CHECKER[name] = PathCommandDependency( + CHECKER[name] = VersionPlistCommandDependency( command=None, paths=paths, name=name, long_name=long_name) del paths, name, long_name # cleanup namespace @@ -807,9 +849,10 @@ for package,name,long_name,minimum_version,and_dependencies in [ long_name='{0} for IPython'.format( CHECKER['chromium'].long_name), minimum_version=(13, 0)), - PathCommandDependency( + VersionPlistCommandDependency( command=CHECKER['safari'].command, paths=CHECKER['safari'].paths, + key=CHECKER['safari'].key, name='{0}-for-ipython'.format( CHECKER['safari'].name), long_name='{0} for IPython'.format( -- 2.26.2