From 14156218b0013d670a82783e33b14e5a15d1b252 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sun, 25 Jan 2015 15:32:14 -0800 Subject: [PATCH 1/2] 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 @@ def import_module(name): 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 @@ def _get_version(self): 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 @@ def _program_files_paths(*args): 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 @@ def _program_files_paths(*args): 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( From 9bffbc22f007bd7c88e1a246043983fcf53a6528 Mon Sep 17 00:00:00 2001 From: Andrew Walker Date: Mon, 26 Jan 2015 18:52:04 +0000 Subject: [PATCH 2/2] Fix decorator in VersionPlistCommandDependency This class includes a static method that calls a static method and this fails with: $ ./swc-installation-test-2.py safari check Safari (safari)... Traceback (most recent call last): File "./swc-installation-test-2.py", line 1007, in passed = check(args) File "./swc-installation-test-2.py", line 243, in check version = checker.check() File "./swc-installation-test-2.py", line 298, in check return self._check() File "./swc-installation-test-2.py", line 340, in _check version = self._get_version() File "./swc-installation-test-2.py", line 536, in _get_version return self._get_version_from_plist(path=path) File "./swc-installation-test-2.py", line 527, in _get_version_from_plist value = self._get_next(root=tree, element=key) File "./swc-installation-test-2.py", line 511, in _get_next parent = self._get_parent(root=root, element=element) NameError: global name 'self' is not defined Change the static method _get_next (which is not passed self when called) to a class method (which is passed the class object) so that we can call the _get_parent static method (whatever the class is called). --- swc-installation-test-2.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/swc-installation-test-2.py b/swc-installation-test-2.py index 54e5250..053bab6 100755 --- a/swc-installation-test-2.py +++ b/swc-installation-test-2.py @@ -504,11 +504,11 @@ def _get_parent(root, element): return node raise ValueError((root, element)) - @staticmethod - def _get_next(root, element): + @classmethod + def _get_next(cls, root, element): """Returns the following sibling of this element or None """ - parent = self._get_parent(root=root, element=element) + parent = cls._get_parent(root=root, element=element) siblings = iter(parent) for node in siblings: if node == element: