diff --git a/README.md b/README.md
index 4bd3379a39..ed670f2570 100644
--- a/README.md
+++ b/README.md
@@ -153,6 +153,17 @@ keep-alive background thread that periodically pings ycmd (just call the
You can also turn this off by passing `--idle_suicide_seconds=0`, although that
isn't recommended.
+### Exit codes
+
+During startup, ycmd attempts to load the `ycm_core` library and exits with one
+of the following return codes if unsuccessful:
+
+- 3: unexpected error while loading the library;
+- 4: the `ycm_core` library is missing;
+- 5: the `ycm_core` library is compiled for Python 3 but loaded in Python 2;
+- 6: the `ycm_core` library is compiled for Python 2 but loaded in Python 3;
+- 7: the version of the `ycm_core` library is outdated.
+
User-level customization
-----------------------
diff --git a/check_core_version.py b/check_core_version.py
deleted file mode 100755
index 16399f6640..0000000000
--- a/check_core_version.py
+++ /dev/null
@@ -1,44 +0,0 @@
-# Copyright (C) 2015 Google Inc.
-#
-# This file is part of ycmd.
-#
-# ycmd is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# ycmd is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with ycmd. If not, see .
-
-import sys
-import os
-import ycm_core
-
-VERSION_FILENAME = 'CORE_VERSION'
-
-
-def DirectoryOfThisScript():
- return os.path.dirname( os.path.abspath( __file__ ) )
-
-
-def ExpectedCoreVersion():
- return int( open( os.path.join( DirectoryOfThisScript(),
- VERSION_FILENAME ) ).read() )
-
-
-def CompatibleWithCurrentCoreVersion():
- try:
- current_core_version = ycm_core.YcmCoreVersion()
- except AttributeError:
- return False
- return ExpectedCoreVersion() == current_core_version
-
-
-if not CompatibleWithCurrentCoreVersion():
- sys.exit( 2 )
-sys.exit( 0 )
diff --git a/ycmd/__main__.py b/ycmd/__main__.py
index 411e161ce7..e242904212 100755
--- a/ycmd/__main__.py
+++ b/ycmd/__main__.py
@@ -25,7 +25,7 @@
import os
sys.path.insert( 0, os.path.dirname( os.path.abspath( __file__ ) ) )
-from server_utils import SetUpPythonPath, CompatibleWithCurrentCoreVersion
+from server_utils import SetUpPythonPath, CompatibleWithCurrentCore
SetUpPythonPath()
from future import standard_library
@@ -157,9 +157,9 @@ def Main():
YcmCoreSanityCheck()
extra_conf_store.CallGlobalExtraConfYcmCorePreloadIfExists()
- if not CompatibleWithCurrentCoreVersion():
- # ycm_core.[so|dll|dylib] is too old and needs to be recompiled.
- sys.exit( 2 )
+ code = CompatibleWithCurrentCore()
+ if code:
+ sys.exit( code )
PossiblyDetachFromTerminal()
diff --git a/ycmd/handlers.py b/ycmd/handlers.py
index 425273ece6..98943b5e61 100644
--- a/ycmd/handlers.py
+++ b/ycmd/handlers.py
@@ -23,30 +23,17 @@
standard_library.install_aliases()
from builtins import * # noqa
-from os import path
-
-try:
- import ycm_core
-except ImportError as e:
- raise RuntimeError(
- 'Error importing ycm_core. Are you sure you have placed a '
- 'version 3.2+ libclang.[so|dll|dylib] in folder "{0}"? '
- 'See the Installation Guide in the docs. Full error: {1}'.format(
- path.realpath( path.join( path.abspath( __file__ ), '..', '..' ) ),
- str( e ) ) )
-
import atexit
-import logging
-import json
import bottle
import http.client
+import json
+import logging
import traceback
from bottle import request
-from . import server_state
-from ycmd import user_options_store
+
+import ycm_core
+from ycmd import extra_conf_store, hmac_plugin, server_state, user_options_store
from ycmd.responses import BuildExceptionResponse, BuildCompletionResponse
-from ycmd import hmac_plugin
-from ycmd import extra_conf_store
from ycmd.request_wrap import RequestWrap
from ycmd.bottle_utils import SetResponseHeader
from ycmd.completers.completer_utils import FilterAndSortCandidatesWrap
diff --git a/ycmd/server_utils.py b/ycmd/server_utils.py
index ed08782169..8d3f3fcec8 100644
--- a/ycmd/server_utils.py
+++ b/ycmd/server_utils.py
@@ -22,24 +22,57 @@
# No other imports from `future` because this module is loaded before we have
# put our submodules in sys.path
-import sys
-import os
import io
+import logging
+import os
import re
+import sys
+
+CORE_MISSING_ERROR_REGEX = re.compile( "No module named '?ycm_core'?" )
+CORE_PYTHON2_ERROR_REGEX = re.compile(
+ 'dynamic module does not define (?:init|module export) '
+ 'function \(PyInit_ycm_core\)|'
+ 'Module use of python2[0-9].dll conflicts with this version of Python\.$' )
+CORE_PYTHON3_ERROR_REGEX = re.compile(
+ 'dynamic module does not define init function \(initycm_core\)|'
+ 'Module use of python3[0-9].dll conflicts with this version of Python\.$' )
+
+CORE_MISSING_MESSAGE = (
+ 'ycm_core library not detected; you need to compile it by running the '
+ 'build.py script. See the documentation for more details.' )
+CORE_PYTHON2_MESSAGE = (
+ 'ycm_core library compiled for Python 2 but loaded in Python 3.' )
+CORE_PYTHON3_MESSAGE = (
+ 'ycm_core library compiled for Python 3 but loaded in Python 2.' )
+CORE_OUTDATED_MESSAGE = (
+ 'ycm_core library too old; PLEASE RECOMPILE by running the build.py script. '
+ 'See the documentation for more details.' )
+
+# Exit statuses returned by the CompatibleWithCurrentCore function:
+# - CORE_COMPATIBLE_STATUS: ycm_core is compatible;
+# - CORE_UNEXPECTED_STATUS: unexpected error while loading ycm_core;
+# - CORE_MISSING_STATUS : ycm_core is missing;
+# - CORE_PYTHON2_STATUS : ycm_core is compiled with Python 2 but loaded with
+# Python 3;
+# - CORE_PYTHON3_STATUS : ycm_core is compiled with Python 3 but loaded with
+# Python 2;
+# - CORE_OUTDATED_STATUS : ycm_core version is outdated.
+# Values 1 and 2 are not used because 1 is for general errors and 2 has often a
+# special meaning for Unix programs. See
+# https://docs.python.org/2/library/sys.html#sys.exit
+CORE_COMPATIBLE_STATUS = 0
+CORE_UNEXPECTED_STATUS = 3
+CORE_MISSING_STATUS = 4
+CORE_PYTHON2_STATUS = 5
+CORE_PYTHON3_STATUS = 6
+CORE_OUTDATED_STATUS = 7
VERSION_FILENAME = 'CORE_VERSION'
-CORE_NOT_COMPATIBLE_MESSAGE = (
- 'ycmd can\'t run: ycm_core lib too old, PLEASE RECOMPILE'
-)
DIR_OF_CURRENT_SCRIPT = os.path.dirname( os.path.abspath( __file__ ) )
DIR_PACKAGES_REGEX = re.compile( '(site|dist)-packages$' )
-
-def SetUpPythonPath():
- sys.path.insert( 0, os.path.join( DIR_OF_CURRENT_SCRIPT, '..' ) )
-
- AddNearestThirdPartyFoldersToSysPath( __file__ )
+_logger = logging.getLogger( __name__ )
def ExpectedCoreVersion():
@@ -48,13 +81,49 @@ def ExpectedCoreVersion():
return int( f.read() )
-def CompatibleWithCurrentCoreVersion():
- import ycm_core
+def ImportCore():
+ """Imports and returns the ycm_core module. This function exists for easily
+ mocking this import in tests."""
+ import ycm_core as ycm_core
+ return ycm_core
+
+
+def CompatibleWithCurrentCore():
+ """Checks if ycm_core library is compatible and returns with an exit
+ status."""
+ try:
+ ycm_core = ImportCore()
+ except ImportError as error:
+ message = str( error )
+ if CORE_MISSING_ERROR_REGEX.match( message ):
+ _logger.exception( CORE_MISSING_MESSAGE )
+ return CORE_MISSING_STATUS
+ if CORE_PYTHON2_ERROR_REGEX.match( message ):
+ _logger.exception( CORE_PYTHON2_MESSAGE )
+ return CORE_PYTHON2_STATUS
+ if CORE_PYTHON3_ERROR_REGEX.match( message ):
+ _logger.exception( CORE_PYTHON3_MESSAGE )
+ return CORE_PYTHON3_STATUS
+ _logger.exception( message )
+ return CORE_UNEXPECTED_STATUS
+
try:
current_core_version = ycm_core.YcmCoreVersion()
except AttributeError:
- return False
- return ExpectedCoreVersion() == current_core_version
+ _logger.exception( CORE_OUTDATED_MESSAGE )
+ return CORE_OUTDATED_STATUS
+
+ if ExpectedCoreVersion() != current_core_version:
+ _logger.error( CORE_OUTDATED_MESSAGE )
+ return CORE_OUTDATED_STATUS
+
+ return CORE_COMPATIBLE_STATUS
+
+
+def SetUpPythonPath():
+ sys.path.insert( 0, os.path.join( DIR_OF_CURRENT_SCRIPT, '..' ) )
+
+ AddNearestThirdPartyFoldersToSysPath( __file__ )
def AncestorFolders( path ):
diff --git a/ycmd/tests/check_core_version_test.py b/ycmd/tests/check_core_version_test.py
deleted file mode 100644
index 74347241b4..0000000000
--- a/ycmd/tests/check_core_version_test.py
+++ /dev/null
@@ -1,31 +0,0 @@
-# Copyright (C) 2015 ycmd contributors
-#
-# This file is part of ycmd.
-#
-# ycmd is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# ycmd is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with ycmd. If not, see .
-
-from __future__ import unicode_literals
-from __future__ import print_function
-from __future__ import division
-from __future__ import absolute_import
-from future import standard_library
-standard_library.install_aliases()
-from builtins import * # noqa
-
-from ..server_utils import CompatibleWithCurrentCoreVersion
-from nose.tools import eq_
-
-
-def CompatibleWithCurrentCoreVersion_test():
- eq_( CompatibleWithCurrentCoreVersion(), True )
diff --git a/ycmd/tests/server_utils_test.py b/ycmd/tests/server_utils_test.py
index a24670dca5..43f081f4cd 100644
--- a/ycmd/tests/server_utils_test.py
+++ b/ycmd/tests/server_utils_test.py
@@ -24,13 +24,14 @@
from builtins import * # noqa
from hamcrest import ( assert_that, calling, contains, contains_inanyorder,
- raises )
+ empty, equal_to, has_length, raises )
from mock import patch
from nose.tools import ok_
import os.path
import sys
from ycmd.server_utils import ( AddNearestThirdPartyFoldersToSysPath,
+ CompatibleWithCurrentCore,
PathToNearestThirdPartyFolder )
DIR_OF_THIRD_PARTY = os.path.abspath(
@@ -50,6 +51,111 @@
)
+@patch( 'ycmd.server_utils._logger', autospec = True )
+def RunCompatibleWithCurrentCoreImportException( test, logger ):
+ with patch( 'ycmd.server_utils.ImportCore',
+ side_effect = ImportError( test[ 'exception_message' ] ) ):
+ assert_that( CompatibleWithCurrentCore(),
+ equal_to( test[ 'exit_status' ] ) )
+
+ assert_that( logger.method_calls, has_length( 1 ) )
+ logger.exception.assert_called_with( test[ 'logged_message' ] )
+
+
+@patch( 'ycmd.server_utils._logger', autospec = True )
+def CompatibleWithCurrentCore_Compatible_test( logger ):
+ assert_that( CompatibleWithCurrentCore(), equal_to( 0 ) )
+ assert_that( logger.method_calls, empty() )
+
+
+def CompatibleWithCurrentCore_Unexpected_test():
+ RunCompatibleWithCurrentCoreImportException( {
+ 'exception_message': 'unexpected import exception',
+ 'exit_status': 3,
+ 'logged_message': 'unexpected import exception'
+ } )
+
+
+def CompatibleWithCurrentCore_Missing_test():
+ import_errors = [
+ # Raised by Python 2.
+ 'No module named ycm_core',
+ # Raised by Python 3.
+ "No module named 'ycm_core'"
+ ]
+
+ for error in import_errors:
+ yield RunCompatibleWithCurrentCoreImportException, {
+ 'exception_message': error,
+ 'exit_status': 4,
+ 'logged_message': 'ycm_core library not detected; you need to compile it '
+ 'by running the build.py script. See the documentation '
+ 'for more details.'
+ }
+
+
+def CompatibleWithCurrentCore_Python2_test():
+ import_exception_messages = [
+ # Raised on Linux and OS X with Python 3.3 and 3.4.
+ 'dynamic module does not define init function (PyInit_ycm_core).',
+ # Raised on Linux and OS X with Python 3.5.
+ 'dynamic module does not define module export function (PyInit_ycm_core).',
+ # Raised on Windows.
+ 'Module use of python26.dll conflicts with this version of Python.',
+ 'Module use of python27.dll conflicts with this version of Python.'
+ ]
+
+ for message in import_exception_messages:
+ yield RunCompatibleWithCurrentCoreImportException, {
+ 'exception_message': message,
+ 'exit_status': 5,
+ 'logged_message': 'ycm_core library compiled for Python 2 '
+ 'but loaded in Python 3.'
+ }
+
+
+def CompatibleWithCurrentCore_Python3_test():
+ import_exception_messages = [
+ # Raised on Linux and OS X.
+ 'dynamic module does not define init function (initycm_core).',
+ # Raised on Windows.
+ 'Module use of python34.dll conflicts with this version of Python.',
+ 'Module use of python35.dll conflicts with this version of Python.'
+ ]
+
+ for message in import_exception_messages:
+ yield RunCompatibleWithCurrentCoreImportException, {
+ 'exception_message': message,
+ 'exit_status': 6,
+ 'logged_message': 'ycm_core library compiled for Python 3 '
+ 'but loaded in Python 2.'
+ }
+
+
+@patch( 'ycm_core.YcmCoreVersion', side_effect = AttributeError() )
+@patch( 'ycmd.server_utils._logger', autospec = True )
+def CompatibleWithCurrentCore_Outdated_NoYcmCoreVersionMethod_test( logger,
+ *args ):
+ assert_that( CompatibleWithCurrentCore(), equal_to( 7 ) )
+
+ assert_that( logger.method_calls, has_length( 1 ) )
+ logger.exception.assert_called_with(
+ 'ycm_core library too old; PLEASE RECOMPILE by running the build.py '
+ 'script. See the documentation for more details.' )
+
+
+@patch( 'ycm_core.YcmCoreVersion', return_value = 10 )
+@patch( 'ycmd.server_utils.ExpectedCoreVersion', return_value = 11 )
+@patch( 'ycmd.server_utils._logger', autospec = True )
+def CompatibleWithCurrentCore_Outdated_NoVersionMatch_test( logger, *args ):
+ assert_that( CompatibleWithCurrentCore(), equal_to( 7 ) )
+
+ assert_that( logger.method_calls, has_length( 1 ) )
+ logger.error.assert_called_with(
+ 'ycm_core library too old; PLEASE RECOMPILE by running the build.py '
+ 'script. See the documentation for more details.' )
+
+
def PathToNearestThirdPartyFolder_Success_test():
ok_( PathToNearestThirdPartyFolder( os.path.abspath( __file__ ) ) )