handle nicely typical installation other than debian package / mercurial forest
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Thu, 25 Mar 2010 17:55:24 +0100
changeset 5021 58e89f3dfbae
parent 5017 b2cc2a51706f
child 5022 7e09702aa766
handle nicely typical installation other than debian package / mercurial forest * new _find_prefix method to detect installation prefix (maybe specified using CW_INSTALL_PREFIX environment variable) * set various resource accessors according to this, cleanup on the way * remove no more necessary APYCOT_ROOT hacks
cwconfig.py
cwctl.py
devtools/__init__.py
test/unittest_cwconfig.py
test/unittest_cwctl.py
toolsutils.py
--- a/cwconfig.py	Thu Mar 25 14:33:50 2010 +0100
+++ b/cwconfig.py	Thu Mar 25 17:55:24 2010 +0100
@@ -15,8 +15,7 @@
 
  * cubicweb migration files are by default searched in
    `<CW_SOFTWARE_ROOT>/misc/migration` instead of
-   `/usr/share/cubicweb/migration/`(unless another emplacement is specified
-   using `CW_MIGRATION_DIR`.
+   `<install prefix>/share/cubicweb/migration/`
 
  * Cubicweb will start in 'user' mode (see below)
 
@@ -66,9 +65,6 @@
 .. envvar:: CW_RUNTIME_DIR
    Directory where pid files will be written
 
-.. envvar:: CW_MIGRATION_DIR
-   Directory where cubicweb migration files will be found
-
 
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
@@ -78,12 +74,11 @@
 import sys
 import os
 import logging
-import tempfile
 from smtplib import SMTP
 from threading import Lock
-from os.path import exists, join, expanduser, abspath, normpath, basename, isdir
+from os.path import (exists, join, expanduser, abspath, normpath,
+                     basename, isdir, dirname)
 from warnings import warn
-
 from logilab.common.decorators import cached, classproperty
 from logilab.common.deprecation import deprecated
 from logilab.common.logging_ext import set_log_methods, init_log
@@ -131,6 +126,23 @@
                                  % (directory, modes))
     return modes[0]
 
+def _find_prefix(start_path=CW_SOFTWARE_ROOT):
+    """Runs along the parent directories of *start_path* (default to cubicweb source directory)
+    looking for one containing a 'share/cubicweb' directory.
+    The first matching directory is assumed as the prefix installation of cubicweb
+
+    Returns the matching prefix or None.
+    """
+    prefix = start_path
+    old_prefix = None
+    if not isdir(start_path):
+        prefix = dirname(start_path)
+    while not isdir(join(prefix, 'share', 'cubicweb')) and prefix != old_prefix:
+        old_prefix = prefix
+        prefix = dirname(prefix)
+    if isdir(join(prefix, 'share', 'cubicweb')):
+        return prefix
+    return sys.prefix
 
 # persistent options definition
 PERSISTENT_OPTIONS = (
@@ -203,6 +215,11 @@
 
 CWDEV = exists(join(CW_SOFTWARE_ROOT, '.hg'))
 
+try:
+    _INSTALL_PREFIX = os.environ['CW_INSTALL_PREFIX']
+except KeyError:
+    _INSTALL_PREFIX = _find_prefix()
+
 class CubicWebNoAppConfiguration(ConfigurationMixIn):
     """base class for cubicweb configuration without a specific instance directory
     """
@@ -216,25 +233,16 @@
     # debug mode
     debugmode = False
 
-    if os.environ.get('APYCOT_ROOT'):
-        mode = 'test'
-        # allow to test cubes within apycot using cubicweb not installed by
-        # apycot
-        if __file__.startswith(os.environ['APYCOT_ROOT']):
-            CUBES_DIR = '%(APYCOT_ROOT)s/local/share/cubicweb/cubes/' % os.environ
-            # create __init__ file
-            file(join(CUBES_DIR, '__init__.py'), 'w').close()
-        else:
-            CUBES_DIR = '/usr/share/cubicweb/cubes/'
-    elif (CWDEV and _forced_mode != 'system'):
+
+    if (CWDEV and _forced_mode != 'system'):
         mode = 'user'
-        CUBES_DIR = abspath(normpath(join(CW_SOFTWARE_ROOT, '../cubes')))
+        _CUBES_DIR = join(CW_SOFTWARE_ROOT, '../cubes')
     else:
-        if _forced_mode == 'user':
-            mode = 'user'
-        else:
-            mode = 'system'
-        CUBES_DIR = '/usr/share/cubicweb/cubes/'
+        mode = _forced_mode or 'system'
+        _CUBES_DIR = join(_INSTALL_PREFIX, 'cubes')
+
+    CUBES_DIR = env_path('CW_CUBES_DIR', _CUBES_DIR, 'cubes', checkexists=False)
+    CUBES_PATH = os.environ.get('CW_CUBES_PATH', '').split(os.pathsep)
 
     options = (
        ('log-threshold',
@@ -296,7 +304,6 @@
           }),
         )
     # static and class methods used to get instance independant resources ##
-
     @staticmethod
     def cubicweb_version():
         """return installed cubicweb version"""
@@ -343,13 +350,10 @@
     def cubes_search_path(cls):
         """return the path of directories where cubes should be searched"""
         path = []
-        try:
-            for directory in os.environ['CW_CUBES_PATH'].split(os.pathsep):
-                directory = abspath(normpath(directory))
-                if exists(directory) and not directory in path:
-                    path.append(directory)
-        except KeyError:
-            pass
+        for directory in cls.CUBES_PATH:
+            directory = abspath(normpath(directory))
+            if exists(directory) and not directory in path:
+                path.append(directory)
         if not cls.CUBES_DIR in path and exists(cls.CUBES_DIR):
             path.append(cls.CUBES_DIR)
         return path
@@ -365,7 +369,7 @@
     @classmethod
     def cube_dir(cls, cube):
         """return the cube directory for the given cube id,
-        raise ConfigurationError if it doesn't exists
+        raise `ConfigurationError` if it doesn't exists
         """
         for directory in cls.cubes_search_path():
             cubedir = join(directory, cube)
@@ -383,10 +387,12 @@
         """return the information module for the given cube"""
         cube = CW_MIGRATION_MAP.get(cube, cube)
         try:
-            return getattr(__import__('cubes.%s.__pkginfo__' % cube), cube).__pkginfo__
+            parent = __import__('cubes.%s.__pkginfo__' % cube)
+            return getattr(parent, cube).__pkginfo__
         except Exception, ex:
-            raise ConfigurationError('unable to find packaging information for '
-                                     'cube %s (%s: %s)' % (cube, ex.__class__.__name__, ex))
+            raise ConfigurationError(
+                'unable to find packaging information for cube %s (%s: %s)'
+                % (cube, ex.__class__.__name__, ex))
 
     @classmethod
     def cube_version(cls, cube):
@@ -588,6 +594,7 @@
             cw_rest_init()
 
     def adjust_sys_path(self):
+        # overriden in CubicWebConfiguration
         self.cls_adjust_sys_path()
 
     def init_log(self, logthreshold=None, debug=False,
@@ -637,35 +644,24 @@
         """
         return None
 
+
 class CubicWebConfiguration(CubicWebNoAppConfiguration):
     """base class for cubicweb server and web configurations"""
 
-    INSTANCES_DATA_DIR = None
-    if os.environ.get('APYCOT_ROOT'):
-        root = os.environ['APYCOT_ROOT']
-        REGISTRY_DIR = '%s/etc/cubicweb.d/' % root
-        if not exists(REGISTRY_DIR):
-            os.makedirs(REGISTRY_DIR)
-        RUNTIME_DIR = tempfile.gettempdir()
-        # allow to test cubes within apycot using cubicweb not installed by
-        # apycot
-        if __file__.startswith(os.environ['APYCOT_ROOT']):
-            MIGRATION_DIR = '%s/local/share/cubicweb/migration/' % root
+    if CubicWebNoAppConfiguration.mode == 'user':
+        _INSTANCES_DIR = expanduser('~/etc/cubicweb.d/')
+    else: #mode = 'system'
+        if _INSTALL_PREFIX == '/usr':
+            _INSTANCES_DIR = '/etc/cubicweb.d/'
         else:
-            MIGRATION_DIR = '/usr/share/cubicweb/migration/'
-    else:
-        if CubicWebNoAppConfiguration.mode == 'user':
-            REGISTRY_DIR = expanduser('~/etc/cubicweb.d/')
-            RUNTIME_DIR = tempfile.gettempdir()
-            INSTANCES_DATA_DIR = REGISTRY_DIR
-        else: #mode = 'system'
-            REGISTRY_DIR = '/etc/cubicweb.d/'
-            RUNTIME_DIR = '/var/run/cubicweb/'
-            INSTANCES_DATA_DIR = '/var/lib/cubicweb/instances/'
-        if CWDEV:
-            MIGRATION_DIR = join(CW_SOFTWARE_ROOT, 'misc', 'migration')
-        else:
-            MIGRATION_DIR = '/usr/share/cubicweb/migration/'
+            _INSTANCES_DIR = join(_INSTALL_PREFIX, 'etc', 'cubicweb.d')
+
+    if os.environ.get('APYCOT_ROOT'):
+        _cubes_init = join(CubicWebNoAppConfiguration.CUBES_DIR, '__init__.py')
+        if not exists(_cubes_init):
+            file(join(_cubes_init), 'w').close()
+        if not exists(_INSTANCES_DIR):
+            os.makedirs(_INSTANCES_DIR)
 
     # for some commands (creation...) we don't want to initialize gettext
     set_language = True
@@ -711,25 +707,19 @@
         )
 
     @classmethod
-    def runtime_dir(cls):
-        """run time directory for pid file..."""
-        return env_path('CW_RUNTIME_DIR', cls.RUNTIME_DIR, 'run time')
-
-    @classmethod
-    def registry_dir(cls):
+    def instances_dir(cls):
         """return the control directory"""
-        return env_path('CW_INSTANCES_DIR', cls.REGISTRY_DIR, 'registry')
-
-    @classmethod
-    def instance_data_dir(cls):
-        """return the instance data directory"""
-        return env_path('CW_INSTANCES_DATA_DIR', cls.INSTANCES_DATA_DIR,
-                        'additional data')
+        return env_path('CW_INSTANCES_DIR', cls._INSTANCES_DIR, 'registry')
 
     @classmethod
     def migration_scripts_dir(cls):
         """cubicweb migration scripts directory"""
-        return env_path('CW_MIGRATION_DIR', cls.MIGRATION_DIR, 'migration')
+        if CWDEV:
+            return join(CW_SOFTWARE_ROOT, 'misc', 'migration')
+        mdir = join(_INSTALL_PREFIX, 'share', 'cubicweb', 'migration')
+        if not exists(path):
+            raise ConfigurationError('migration path %s doesn\'t exist' % mdir)
+        return mdir
 
     @classmethod
     def config_for(cls, appid, config=None):
@@ -752,9 +742,10 @@
         """return the home directory of the instance with the given
         instance id
         """
-        home = join(cls.registry_dir(), appid)
+        home = join(cls.instances_dir(), appid)
         if not exists(home):
-            raise ConfigurationError('no such instance %s (check it exists with "cubicweb-ctl list")' % appid)
+            raise ConfigurationError('no such instance %s (check it exists with'
+                                     ' "cubicweb-ctl list")' % appid)
         return home
 
     MODES = ('common', 'repository', 'Any', 'web')
@@ -777,7 +768,9 @@
     def default_log_file(self):
         """return default path to the log file of the instance'server"""
         if self.mode == 'user':
-            basepath = join(tempfile.gettempdir(), '%s-%s' % (basename(self.appid), self.name))
+            import tempfile
+            basepath = join(tempfile.gettempdir(), '%s-%s' % (
+                basename(self.appid), self.name))
             path = basepath + '.log'
             i = 1
             while exists(path) and i < 100: # arbitrary limit to avoid infinite loop
@@ -792,7 +785,13 @@
 
     def default_pid_file(self):
         """return default path to the pid file of the instance'server"""
-        return join(self.runtime_dir(), '%s-%s.pid' % (self.appid, self.name))
+        if self.mode == 'system':
+            # XXX not under _INSTALL_PREFIX, right?
+            rtdir = env_path('CW_RUNTIME_DIR', '/var/run/cubicweb/', 'run time')
+        else:
+            import tempfile
+            rtdir = env_path('CW_RUNTIME_DIR', tempfile.gettempdir(), 'run time')
+        return join(rtdir, '%s-%s.pid' % (self.appid, self.name))
 
     # instance methods used to get instance specific resources #############
 
@@ -812,11 +811,17 @@
 
     @property
     def apphome(self):
-        return join(self.registry_dir(), self.appid)
+        return join(self.instances_dir(), self.appid)
 
     @property
     def appdatahome(self):
-        return join(self.instance_data_dir(), self.appid)
+        if self.mode == 'system':
+            # XXX not under _INSTALL_PREFIX, right?
+            iddir = '/var/lib/cubicweb/instances/'
+        else:
+            iddir = self.instances_dir()
+        iddir = env_path('CW_INSTANCES_DATA_DIR', iddir, 'additional data')
+        return join(iddir, self.appid)
 
     def init_cubes(self, cubes):
         assert self._cubes is None, self._cubes
--- a/cwctl.py	Thu Mar 25 14:33:50 2010 +0100
+++ b/cwctl.py	Thu Mar 25 17:55:24 2010 +0100
@@ -85,7 +85,7 @@
         Instance used by another one should appears first in the file (one
         instance per line)
         """
-        regdir = cwcfg.registry_dir()
+        regdir = cwcfg.instances_dir()
         _allinstances = list_instances(regdir)
         if isfile(join(regdir, 'startorder')):
             allinstances = []
@@ -303,7 +303,7 @@
                     print '    available modes: %s' % ', '.join(modes)
         print
         try:
-            regdir = cwcfg.registry_dir()
+            regdir = cwcfg.instances_dir()
         except ConfigurationError, ex:
             print 'No instance available:', ex
             print
@@ -612,7 +612,7 @@
     actionverb = 'restarted'
 
     def run_args(self, args, askconfirm):
-        regdir = cwcfg.registry_dir()
+        regdir = cwcfg.instances_dir()
         if not isfile(join(regdir, 'startorder')) or len(args) <= 1:
             # no specific startorder
             super(RestartInstanceCommand, self).run_args(args, askconfirm)
@@ -958,7 +958,7 @@
 
     def run(self, args):
         """run the command with its specific arguments"""
-        regdir = cwcfg.registry_dir()
+        regdir = cwcfg.instances_dir()
         for appid in sorted(listdir(regdir)):
             print appid
 
--- a/devtools/__init__.py	Thu Mar 25 14:33:50 2010 +0100
+++ b/devtools/__init__.py	Thu Mar 25 17:55:24 2010 +0100
@@ -98,9 +98,6 @@
           }),
         ))
 
-    if not os.environ.get('APYCOT_ROOT'):
-        REGISTRY_DIR = normpath(join(CW_SOFTWARE_ROOT, '../cubes'))
-
     def __init__(self, appid, log_threshold=logging.CRITICAL+10):
         ServerConfiguration.__init__(self, appid)
         self.init_log(log_threshold, force=True)
--- a/test/unittest_cwconfig.py	Thu Mar 25 14:33:50 2010 +0100
+++ b/test/unittest_cwconfig.py	Thu Mar 25 17:55:24 2010 +0100
@@ -7,13 +7,16 @@
 """
 import sys
 import os
+import tempfile
 from os.path import dirname, join, abspath
 
 from logilab.common.modutils import cleanup_sys_modules
-from logilab.common.testlib import TestCase, unittest_main
+from logilab.common.testlib import (TestCase, unittest_main,
+                                    with_tempdir)
 from logilab.common.changelog import Version
 
 from cubicweb.devtools import ApptestConfiguration
+from cubicweb.cwconfig import _find_prefix
 
 def unabsolutize(path):
     parts = path.split(os.sep)
@@ -32,7 +35,7 @@
         self.config._cubes = ('email', 'file')
 
     def tearDown(self):
-        os.environ.pop('CW_CUBES_PATH', None)
+        ApptestConfiguration.CUBES_PATH = []
 
     def test_reorder_cubes(self):
         # jpl depends on email and file and comment
@@ -91,11 +94,11 @@
         # make sure we don't import the email cube, but the stdlib email package
         import email
         self.assertNotEquals(dirname(email.__file__), self.config.CUBES_DIR)
-        os.environ['CW_CUBES_PATH'] = CUSTOM_CUBES_DIR
+        self.config.__class__.CUBES_PATH = [CUSTOM_CUBES_DIR]
         self.assertEquals(self.config.cubes_search_path(),
                           [CUSTOM_CUBES_DIR, self.config.CUBES_DIR])
-        os.environ['CW_CUBES_PATH'] = os.pathsep.join([
-            CUSTOM_CUBES_DIR, self.config.CUBES_DIR, 'unexistant'])
+        self.config.__class__.CUBES_PATH = [CUSTOM_CUBES_DIR,
+                                            self.config.CUBES_DIR, 'unexistant']
         # filter out unexistant and duplicates
         self.assertEquals(self.config.cubes_search_path(),
                           [CUSTOM_CUBES_DIR,
@@ -114,6 +117,91 @@
         from cubes import file
         self.assertEquals(file.__path__, [join(CUSTOM_CUBES_DIR, 'file')])
 
+class FindPrefixTC(TestCase):
+    def make_dirs(self, *args):
+        path = join(tempfile.tempdir, *args)
+        if not os.path.exists(path):
+            os.makedirs(path)
+        return path
+
+    def make_file(self, *args):
+        self.make_dirs(*args[: -1])
+        file_path = join(tempfile.tempdir, *args)
+        file_obj = open(file_path, 'w')
+        file_obj.write('""" None """')
+        file_obj.close()
+        return file_path
+
+    @with_tempdir
+    def test_samedir(self):
+        prefix = tempfile.tempdir
+        self.make_dirs('share', 'cubicweb')
+        self.assertEquals(_find_prefix(prefix), prefix)
+
+    @with_tempdir
+    def test_samedir_filepath(self):
+        prefix = tempfile.tempdir
+        self.make_dirs('share', 'cubicweb')
+        file_path = self.make_file('bob.py')
+        self.assertEquals(_find_prefix(file_path), prefix)
+
+    @with_tempdir
+    def test_dir_inside_prefix(self):
+        prefix = tempfile.tempdir
+        self.make_dirs('share', 'cubicweb')
+        dir_path = self.make_dirs('bob')
+        self.assertEquals(_find_prefix(dir_path), prefix)
+
+    @with_tempdir
+    def test_file_in_dir_inside_prefix(self):
+        prefix = tempfile.tempdir
+        self.make_dirs('share', 'cubicweb')
+        file_path = self.make_file('bob', 'toto.py')
+        self.assertEquals(_find_prefix(file_path), prefix)
+
+    @with_tempdir
+    def test_file_in_deeper_dir_inside_prefix(self):
+        prefix = tempfile.tempdir
+        self.make_dirs('share', 'cubicweb')
+        file_path = self.make_file('bob', 'pyves', 'alain', 'adim', 'syt', 'toto.py')
+        self.assertEquals(_find_prefix(file_path), prefix)
+
+    @with_tempdir
+    def test_multiple_candidate_prefix(self):
+        self.make_dirs('share', 'cubicweb')
+        prefix = self.make_dirs('bob')
+        self.make_dirs('bob', 'share', 'cubicweb')
+        file_path = self.make_file('bob', 'pyves', 'alain', 'adim', 'syt', 'toto.py')
+        self.assertEquals(_find_prefix(file_path), prefix)
+
+    @with_tempdir
+    def test_sister_candidate_prefix(self):
+        prefix = tempfile.tempdir
+        self.make_dirs('share', 'cubicweb')
+        self.make_dirs('bob', 'share', 'cubicweb')
+        file_path = self.make_file('bell', 'toto.py')
+        self.assertEquals(_find_prefix(file_path), prefix)
+
+    @with_tempdir
+    def test_multiple_parent_candidate_prefix(self):
+        self.make_dirs('share', 'cubicweb')
+        prefix = self.make_dirs('share', 'cubicweb', 'bob')
+        self.make_dirs('share', 'cubicweb', 'bob', 'share', 'cubicweb')
+        file_path = self.make_file('share', 'cubicweb', 'bob', 'pyves', 'alain', 'adim', 'syt', 'toto.py')
+        self.assertEquals(_find_prefix(file_path), prefix)
+
+    @with_tempdir
+    def test_upper_candidate_prefix(self):
+        prefix = tempfile.tempdir
+        self.make_dirs('share', 'cubicweb')
+        self.make_dirs('bell','bob',  'share', 'cubicweb')
+        file_path = self.make_file('bell', 'toto.py')
+        self.assertEquals(_find_prefix(file_path), prefix)
+
+    @with_tempdir
+    def test_no_prefix(self):
+        prefix = tempfile.tempdir
+        self.assertEquals(_find_prefix(prefix), sys.prefix)
 
 if __name__ == '__main__':
     unittest_main()
--- a/test/unittest_cwctl.py	Thu Mar 25 14:33:50 2010 +0100
+++ b/test/unittest_cwctl.py	Thu Mar 25 17:55:24 2010 +0100
@@ -10,15 +10,8 @@
 from cStringIO import StringIO
 from logilab.common.testlib import TestCase, unittest_main
 
-if os.environ.get('APYCOT_ROOT'):
-    root = os.environ['APYCOT_ROOT']
-    CUBES_DIR = '%s/local/share/cubicweb/cubes/' % root
-    os.environ['CW_CUBES_PATH'] = CUBES_DIR
-    REGISTRY_DIR = '%s/etc/cubicweb.d/' % root
-    os.environ['CW_INSTANCES_DIR'] = REGISTRY_DIR
-
 from cubicweb.cwconfig import CubicWebConfiguration
-CubicWebConfiguration.load_cwctl_plugins()
+CubicWebConfiguration.load_cwctl_plugins() # XXX necessary?
 
 class CubicWebCtlTC(TestCase):
     def setUp(self):
--- a/toolsutils.py	Thu Mar 25 14:33:50 2010 +0100
+++ b/toolsutils.py	Thu Mar 25 17:55:24 2010 +0100
@@ -184,7 +184,7 @@
                 config_file, ex)
     return config
 
-def env_path(env_var, default, name):
+def env_path(env_var, default, name, checkexists=True):
     """get a path specified in a variable or using the default value and return
     it.
 
@@ -203,8 +203,8 @@
     :raise `ConfigurationError`: if the returned path does not exist
     """
     path = environ.get(env_var, default)
-    if not exists(path):
-        raise ConfigurationError('%s path %s doesn\'t exist' % (name, path))
+    if checkexists and not exists(path):
+        raise ConfigurationError('%s directory %s doesn\'t exist' % (name, path))
     return abspath(path)