diff --git a/mkbinary.sh b/mkbinary.sh index 67829074..86cf8215 100755 --- a/mkbinary.sh +++ b/mkbinary.sh @@ -1,5 +1,5 @@ #!/bin/sh set -e -pip install --ignore-installed -r requirements-bin.txt +pip install --ignore-installed setuptools==19.2 pyinstaller pyinstaller --clean --onefile patroni.spec diff --git a/patroni.spec b/patroni.spec index 8c5c5347..2afe8eac 100644 --- a/patroni.spec +++ b/patroni.spec @@ -3,11 +3,21 @@ block_cipher = None -a = Analysis(['patroni/__main__.py', 'patroni/dcs/consul.py', 'patroni/dcs/etcd.py', 'patroni/dcs/exhibitor.py', 'patroni/dcs/zookeeper.py'], +def hiddenimports(): + import sys + sys.path.insert(0, '.') + try: + import patroni.dcs + return patroni.dcs.dcs_modules() + finally: + sys.path.pop(0) + + +a = Analysis(['patroni/__main__.py'], pathex=[], binaries=None, datas=None, - hiddenimports=['patroni.dcs.consul', 'patroni.dcs.etcd', 'patroni.dcs.exhibitor', 'patroni.dcs.zookeeper'], + hiddenimports=hiddenimports(), hookspath=[], runtime_hooks=[], excludes=[], diff --git a/patroni/dcs/__init__.py b/patroni/dcs/__init__.py index 0045083c..733efbb0 100644 --- a/patroni/dcs/__init__.py +++ b/patroni/dcs/__init__.py @@ -34,26 +34,29 @@ def parse_connection_string(value): def dcs_modules(): """Get names of DCS modules, depending on execution environment. If being packaged with PyInstaller, - modules aren't discoverable dynamically by scanning source directory. Thus, when running in bundle, - a predefined list of dcs modules is returned. See: - https://pyinstaller.readthedocs.io/en/stable/runtime-information.html#run-time-information""" + modules aren't discoverable dynamically by scanning source directory because `FrozenImporter` doesn't + implement `iter_modules` method. But it is still possible to find all potential DCS modules by + iterating through `toc`, which contains list of all "frozen" resources.""" + + dcs_dirname = os.path.dirname(__file__) + module_prefix = __package__ + '.' if getattr(sys, 'frozen', False): - return ['consul', 'etcd', 'zookeeper', 'exhibitor'] + importer = pkgutil.get_importer(dcs_dirname) + return [module for module in list(importer.toc) if module.startswith(module_prefix) and module.count('.') == 2] else: - module_names = (name for _, name, is_pkg in pkgutil.iter_modules([os.path.dirname(__file__)]) if not is_pkg) - return module_names + return [module_prefix + name for _, name, is_pkg in pkgutil.iter_modules([dcs_dirname]) if not is_pkg] def get_dcs(config): available_implementations = set() for module_name in dcs_modules(): - module = importlib.import_module(__package__ + '.' + module_name) + module = importlib.import_module(module_name) for name in filter(lambda name: not name.startswith('__'), dir(module)): # iterate through module content value = getattr(module, name) name = name.lower() # try to find implementation of AbstractDCS interface, class name must match with module_name - if inspect.isclass(value) and issubclass(value, AbstractDCS) and name == module_name: + if inspect.isclass(value) and issubclass(value, AbstractDCS) and __package__ + '.' + name == module_name: available_implementations.add(name) if name in config: # which has configuration section in the config file # propagate some parameters diff --git a/requirements-bin.txt b/requirements-bin.txt deleted file mode 100644 index 7e43a3fd..00000000 --- a/requirements-bin.txt +++ /dev/null @@ -1,2 +0,0 @@ -setuptools==19.2 -pyinstaller diff --git a/tests/test_patroni.py b/tests/test_patroni.py index f9f135cc..d9c0e93a 100644 --- a/tests/test_patroni.py +++ b/tests/test_patroni.py @@ -14,6 +14,11 @@ from test_etcd import SleepException, etcd_read, etcd_write from test_postgresql import Postgresql, psycopg2_connect +class MockFrozenImporter(object): + + toc = set(['patroni.dcs.etcd']) + + @patch('time.sleep', Mock()) @patch('subprocess.call', Mock(return_value=0)) @patch('psycopg2.connect', psycopg2_connect) @@ -27,6 +32,8 @@ from test_postgresql import Postgresql, psycopg2_connect @patch.object(etcd.Client, 'read', etcd_read) class TestPatroni(unittest.TestCase): + @patch('pkgutil.get_importer', Mock(return_value=MockFrozenImporter())) + @patch('sys.frozen', Mock(return_value=True), create=True) @patch.object(etcd.Client, 'read', etcd_read) def setUp(self): RestApiServer._BaseServer__is_shut_down = Mock()