cmake: Moved finding and running CMake out of dependency

This commit is contained in:
Daniel Mensinger 2019-06-07 15:53:45 +02:00
parent 44b9421071
commit 6083cfa6c8
No known key found for this signature in database
GPG Key ID: 54DD94C131E277D4
5 changed files with 236 additions and 167 deletions

View File

@ -17,10 +17,12 @@
__all__ = [
'CMakeClient',
'CMakeExecutor',
'CMakeException',
'CMakeInterpreter',
]
from .common import CMakeException
from .client import CMakeClient
from .executor import CMakeExecutor
from .interpreter import CMakeInterpreter

View File

@ -16,8 +16,8 @@
# or an interpreter-based tool.
from .common import CMakeException
from .executor import CMakeExecutor
from ..environment import Environment
from ..dependencies.base import CMakeDependency, ExternalProgram
from ..mesonlib import MachineChoice
from .. import mlog
from contextlib import contextmanager
@ -475,14 +475,11 @@ class CMakeClient:
if self.proc is not None:
raise CMakeException('The CMake server was already started')
for_machine = MachineChoice.HOST # TODO make parameter
cmake_exe, cmake_vers, _ = CMakeDependency.find_cmake_binary(self.env, for_machine)
if cmake_exe is None or cmake_exe is False:
raise CMakeException('Unable to find CMake')
assert(isinstance(cmake_exe, ExternalProgram))
cmake_exe = CMakeExecutor(self.env, '>=3.7', for_machine)
if not cmake_exe.found():
raise CMakeException('Unable to find CMake')
mlog.debug('Starting CMake server with CMake', mlog.bold(' '.join(cmake_exe.get_command())), 'version', mlog.cyan(cmake_vers))
mlog.debug('Starting CMake server with CMake', mlog.bold(' '.join(cmake_exe.get_command())), 'version', mlog.cyan(cmake_exe.version()))
self.proc = Popen(cmake_exe.get_command() + ['-E', 'server', '--experimental', '--debug'], stdin=PIPE, stdout=PIPE)
def shutdown(self) -> None:

View File

@ -0,0 +1,215 @@
# Copyright 2019 The Meson development team
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# This class contains the basic functionality needed to run any interpreter
# or an interpreter-based tool.
from .. import mlog, mesonlib
from ..mesonlib import PerMachine, Popen_safe, version_compare, MachineChoice
from ..environment import Environment
from typing import List, Tuple, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from ..dependencies.base import ExternalProgram
import re, os, ctypes
class CMakeExecutor:
# The class's copy of the CMake path. Avoids having to search for it
# multiple times in the same Meson invocation.
class_cmakebin = PerMachine(None, None)
class_cmakevers = PerMachine(None, None)
class_cmake_cache = {}
def __init__(self, environment: Environment, version: str, for_machine: MachineChoice, silent: bool = False):
self.min_version = version
self.environment = environment
self.for_machine = for_machine
self.cmakebin, self.cmakevers = self.find_cmake_binary(self.environment, silent=silent)
if self.cmakebin is False:
self.cmakebin = None
return
if not version_compare(self.cmakevers, self.min_version):
self.cmakebin = None
mlog.warning(
'The version of CMake', mlog.bold(self.cmakebin.get_path()),
'is', mlog.bold(self.cmakevers), 'but version', mlog.bold(self.min_version),
'is required')
return
def find_cmake_binary(self, environment: Environment, silent: bool = False) -> Tuple['ExternalProgram', str]:
from ..dependencies.base import ExternalProgram
# Create an iterator of options
def search():
# Lookup in cross or machine file.
potential_cmakepath = environment.binaries[self.for_machine].lookup_entry('cmake')
if potential_cmakepath is not None:
mlog.debug('CMake binary for %s specified from cross file, native file, or env var as %s.', self.for_machine, potential_cmakepath)
yield ExternalProgram.from_entry('cmake', potential_cmakepath)
# We never fallback if the user-specified option is no good, so
# stop returning options.
return
mlog.debug('CMake binary missing from cross or native file, or env var undefined.')
# Fallback on hard-coded defaults.
# TODO prefix this for the cross case instead of ignoring thing.
if environment.machines.matches_build_machine(self.for_machine):
for potential_cmakepath in environment.default_cmake:
mlog.debug('Trying a default CMake fallback at', potential_cmakepath)
yield ExternalProgram(potential_cmakepath, silent=True)
# Only search for CMake the first time and store the result in the class
# definition
if CMakeExecutor.class_cmakebin[self.for_machine] is False:
mlog.debug('CMake binary for %s is cached as not found' % self.for_machine)
elif CMakeExecutor.class_cmakebin[self.for_machine] is not None:
mlog.debug('CMake binary for %s is cached.' % self.for_machine)
else:
assert CMakeExecutor.class_cmakebin[self.for_machine] is None
mlog.debug('CMake binary for %s is not cached' % self.for_machine)
for potential_cmakebin in search():
mlog.debug('Trying CMake binary {} for machine {} at {}'
.format(potential_cmakebin.name, self.for_machine, potential_cmakebin.command))
version_if_ok = self.check_cmake(potential_cmakebin)
if not version_if_ok:
continue
if not silent:
mlog.log('Found CMake:', mlog.bold(potential_cmakebin.get_path()),
'(%s)' % version_if_ok)
CMakeExecutor.class_cmakebin[self.for_machine] = potential_cmakebin
CMakeExecutor.class_cmakevers[self.for_machine] = version_if_ok
break
else:
if not silent:
mlog.log('Found CMake:', mlog.red('NO'))
# Set to False instead of None to signify that we've already
# searched for it and not found it
CMakeExecutor.class_cmakebin[self.for_machine] = False
CMakeExecutor.class_cmakevers[self.for_machine] = None
return CMakeExecutor.class_cmakebin[self.for_machine], CMakeExecutor.class_cmakevers[self.for_machine]
def check_cmake(self, cmakebin: 'ExternalProgram') -> Optional[str]:
if not cmakebin.found():
mlog.log('Did not find CMake {!r}'.format(cmakebin.name))
return None
try:
p, out = Popen_safe(cmakebin.get_command() + ['--version'])[0:2]
if p.returncode != 0:
mlog.warning('Found CMake {!r} but couldn\'t run it'
''.format(' '.join(cmakebin.get_command())))
return None
except FileNotFoundError:
mlog.warning('We thought we found CMake {!r} but now it\'s not there. How odd!'
''.format(' '.join(cmakebin.get_command())))
return None
except PermissionError:
msg = 'Found CMake {!r} but didn\'t have permissions to run it.'.format(' '.join(cmakebin.get_command()))
if not mesonlib.is_windows():
msg += '\n\nOn Unix-like systems this is often caused by scripts that are not executable.'
mlog.warning(msg)
return None
cmvers = re.sub(r'\s*cmake version\s*', '', out.split('\n')[0]).strip()
return cmvers
def _cache_key(self, args: List[str], build_dir: str, env):
fenv = frozenset(env.items()) if env is not None else None
targs = tuple(args)
return (self.cmakebin, targs, build_dir, fenv)
def _call_real(self, args: List[str], build_dir: str, env) -> Tuple[int, str, str]:
os.makedirs(build_dir, exist_ok=True)
cmd = self.cmakebin.get_command() + args
p, out, err = Popen_safe(cmd, env=env, cwd=build_dir)
rc = p.returncode
call = ' '.join(cmd)
mlog.debug("Called `{}` in {} -> {}".format(call, build_dir, rc))
return rc, out, err
def call(self, args: List[str], build_dir: str, env=None, disable_cache: bool = False):
if env is None:
env = os.environ
if disable_cache:
return self._call_real(args, build_dir, env)
# First check if cached, if not call the real cmake function
cache = CMakeExecutor.class_cmake_cache
key = self._cache_key(args, build_dir, env)
if key not in cache:
cache[key] = self._call_real(args, build_dir, env)
return cache[key]
def call_with_fake_build(self, args: List[str], build_dir: str, env=None):
# First check the cache
cache = CMakeExecutor.class_cmake_cache
key = self._cache_key(args, build_dir, env)
if key in cache:
return cache[key]
os.makedirs(build_dir, exist_ok=True)
# Reset the CMake cache
with open('{}/CMakeCache.txt'.format(build_dir), 'w') as fp:
fp.write('CMAKE_PLATFORM_INFO_INITIALIZED:INTERNAL=1\n')
# Fake the compiler files
comp_dir = '{}/CMakeFiles/{}'.format(build_dir, self.cmakevers)
os.makedirs(comp_dir, exist_ok=True)
c_comp = '{}/CMakeCCompiler.cmake'.format(comp_dir)
cxx_comp = '{}/CMakeCXXCompiler.cmake'.format(comp_dir)
if not os.path.exists(c_comp):
with open(c_comp, 'w') as fp:
fp.write('''# Fake CMake file to skip the boring and slow stuff
set(CMAKE_C_COMPILER "{}") # Just give CMake a valid full path to any file
set(CMAKE_C_COMPILER_ID "GNU") # Pretend we have found GCC
set(CMAKE_COMPILER_IS_GNUCC 1)
set(CMAKE_C_COMPILER_LOADED 1)
set(CMAKE_C_COMPILER_WORKS TRUE)
set(CMAKE_C_ABI_COMPILED TRUE)
set(CMAKE_SIZEOF_VOID_P "{}")
'''.format(os.path.realpath(__file__), ctypes.sizeof(ctypes.c_voidp)))
if not os.path.exists(cxx_comp):
with open(cxx_comp, 'w') as fp:
fp.write('''# Fake CMake file to skip the boring and slow stuff
set(CMAKE_CXX_COMPILER "{}") # Just give CMake a valid full path to any file
set(CMAKE_CXX_COMPILER_ID "GNU") # Pretend we have found GCC
set(CMAKE_COMPILER_IS_GNUCXX 1)
set(CMAKE_CXX_COMPILER_LOADED 1)
set(CMAKE_CXX_COMPILER_WORKS TRUE)
set(CMAKE_CXX_ABI_COMPILED TRUE)
set(CMAKE_SIZEOF_VOID_P "{}")
'''.format(os.path.realpath(__file__), ctypes.sizeof(ctypes.c_voidp)))
return self.call(args, build_dir, env)
def found(self) -> bool:
return self.cmakebin is not None
def version(self) -> str:
return self.cmakevers
def executable_path(self) -> str:
return self.cmakebin.get_path()
def get_command(self):
return self.cmakebin.get_command()
def machine_choice(self) -> MachineChoice:
return self.for_machine

View File

@ -17,18 +17,20 @@
from .common import CMakeException
from .client import CMakeClient, RequestCMakeInputs, RequestConfigure, RequestCompute, RequestCodeModel, CMakeTarget
from .executor import CMakeExecutor
from .. import mlog
from ..build import Build
from ..environment import Environment
from ..mesonlib import MachineChoice
from ..mparser import Token, BaseNode, CodeBlockNode, FunctionNode, ArrayNode, ArgumentNode, AssignmentNode, BooleanNode, StringNode, IdNode, MethodNode
from ..backend.backends import Backend
from ..compilers.compilers import lang_suffixes, header_suffixes, obj_suffixes
from ..dependencies.base import CMakeDependency, ExternalProgram
from subprocess import Popen, PIPE, STDOUT
from typing import List, Dict, Optional
from typing import List, Dict, Optional, TYPE_CHECKING
import os, re
if TYPE_CHECKING:
from ..build import Build
from ..backend.backends import Backend
backend_generator_map = {
'ninja': 'Ninja',
'xcode': 'Xcode',
@ -276,7 +278,7 @@ class ConverterTarget:
mlog.log(' -', key, '=', mlog.bold(str(val)))
class CMakeInterpreter:
def __init__(self, build: Build, subdir: str, src_dir: str, install_prefix: str, env: Environment, backend: Backend):
def __init__(self, build: 'Build', subdir: str, src_dir: str, install_prefix: str, env: Environment, backend: 'Backend'):
assert(hasattr(backend, 'name'))
self.build = build
self.subdir = subdir
@ -303,10 +305,7 @@ class CMakeInterpreter:
def configure(self, extra_cmake_options: List[str]) -> None:
for_machine = MachineChoice.HOST # TODO make parameter
# Find CMake
cmake_exe, cmake_vers, _ = CMakeDependency.find_cmake_binary(self.env, for_machine)
if cmake_exe is None or cmake_exe is False:
raise CMakeException('Unable to find CMake')
assert(isinstance(cmake_exe, ExternalProgram))
cmake_exe = CMakeExecutor(self.env, '>=3.7', for_machine)
if not cmake_exe.found():
raise CMakeException('Unable to find CMake')
@ -333,7 +332,7 @@ class CMakeInterpreter:
# Run CMake
mlog.log()
with mlog.nested():
mlog.log('Configuring the build directory with', mlog.bold('CMake'), 'version', mlog.cyan(cmake_vers))
mlog.log('Configuring the build directory with', mlog.bold('CMake'), 'version', mlog.cyan(cmake_exe.version()))
mlog.log(mlog.bold('Running:'), ' '.join(cmake_args))
mlog.log()
os.makedirs(self.build_dir, exist_ok=True)

View File

@ -23,8 +23,6 @@ import shlex
import shutil
import textwrap
import platform
import itertools
import ctypes
import typing
from typing import Any, Dict, List, Tuple
from enum import Enum
@ -34,6 +32,7 @@ from .. import mlog
from .. import mesonlib
from ..compilers import clib_langs
from ..environment import BinaryTable, Environment, MachineInfo
from ..cmake import CMakeExecutor, CMakeException
from ..mesonlib import MachineChoice, MesonException, OrderedSet, PerMachine
from ..mesonlib import Popen_safe, version_compare_many, version_compare, listify, stringlistify, extract_as_list
from ..mesonlib import Version, LibType
@ -990,11 +989,7 @@ class CMakeTarget:
class CMakeDependency(ExternalDependency):
# The class's copy of the CMake path. Avoids having to search for it
# multiple times in the same Meson invocation.
class_cmakebin = PerMachine(None, None)
class_cmakevers = PerMachine(None, None)
class_cmakeinfo = PerMachine(None, None)
# We cache all pkg-config subprocess invocations to avoid redundant calls
cmake_cache = {}
# Version string for the minimum CMake version
class_cmake_version = '>=3.4'
# CMake generators to try (empty for no generator)
@ -1029,7 +1024,6 @@ class CMakeDependency(ExternalDependency):
# Store a copy of the CMake path on the object itself so it is
# stored in the pickled coredata and recovered.
self.cmakebin = None
self.cmakevers = None
self.cmakeinfo = None
# Dict of CMake variables: '<var_name>': ['list', 'of', 'values']
@ -1044,10 +1038,10 @@ class CMakeDependency(ExternalDependency):
# List of successfully found modules
self.found_modules = []
self.cmakebin, self.cmakevers, for_machine = self.find_cmake_binary(environment, self.for_machine, self.silent)
if self.cmakebin is False:
self.cmakebin = CMakeExecutor(environment, CMakeDependency.class_cmake_version, self.for_machine, silent=self.silent)
if not self.cmakebin.found():
self.cmakebin = None
msg = 'No CMake binary for machine %s not found. Giving up.' % for_machine
msg = 'No CMake binary for machine %s not found. Giving up.' % self.for_machine
if self.required:
raise DependencyException(msg)
mlog.debug(msg)
@ -1075,57 +1069,6 @@ class CMakeDependency(ExternalDependency):
return
self._detect_dep(name, modules, cm_args)
@classmethod
def find_cmake_binary(cls, environment: Environment, for_machine: MachineChoice, silent: bool = False) -> Tuple[str, str, MachineChoice]:
# Create an iterator of options
def search():
# Lookup in cross or machine file.
potential_cmakepath = environment.binaries[for_machine].lookup_entry('cmake')
if potential_cmakepath is not None:
mlog.debug('CMake binary for %s specified from cross file, native file, or env var as %s.', for_machine, potential_cmakepath)
yield ExternalProgram.from_entry('cmake', potential_cmakepath)
# We never fallback if the user-specified option is no good, so
# stop returning options.
return
mlog.debug('CMake binary missing from cross or native file, or env var undefined.')
# Fallback on hard-coded defaults.
# TODO prefix this for the cross case instead of ignoring thing.
if environment.machines.matches_build_machine(for_machine):
for potential_cmakepath in environment.default_cmake:
mlog.debug('Trying a default CMake fallback at', potential_cmakepath)
yield ExternalProgram(potential_cmakepath, silent=True)
# Only search for CMake the first time and store the result in the class
# definition
if CMakeDependency.class_cmakebin[for_machine] is False:
mlog.debug('CMake binary for %s is cached as not found' % for_machine)
elif CMakeDependency.class_cmakebin[for_machine] is not None:
mlog.debug('CMake binary for %s is cached.' % for_machine)
else:
assert CMakeDependency.class_cmakebin[for_machine] is None
mlog.debug('CMake binary for %s is not cached' % for_machine)
for potential_cmakebin in search():
mlog.debug('Trying CMake binary {} for machine {} at {}'
.format(potential_cmakebin.name, for_machine, potential_cmakebin.command))
version_if_ok = cls.check_cmake(potential_cmakebin)
if not version_if_ok:
continue
if not silent:
mlog.log('Found CMake:', mlog.bold(potential_cmakebin.get_path()),
'(%s)' % version_if_ok)
CMakeDependency.class_cmakebin[for_machine] = potential_cmakebin
CMakeDependency.class_cmakevers[for_machine] = version_if_ok
break
else:
if not silent:
mlog.log('Found CMake:', mlog.red('NO'))
# Set to False instead of None to signify that we've already
# searched for it and not found it
CMakeDependency.class_cmakebin[for_machine] = False
CMakeDependency.class_cmakevers[for_machine] = None
return CMakeDependency.class_cmakebin[for_machine], CMakeDependency.class_cmakevers[for_machine], for_machine
def __repr__(self):
s = '<{0} {1}: {2} {3}>'
return s.format(self.__class__.__name__, self.name, self.is_found,
@ -1305,7 +1248,7 @@ class CMakeDependency(ExternalDependency):
# parameters to stderr as they are executed. Since CMake 3.4.0
# variables ("${VAR}") are also replaced in the trace output.
mlog.debug('\nDetermining dependency {!r} with CMake executable '
'{!r}'.format(name, self.cmakebin.get_path()))
'{!r}'.format(name, self.cmakebin.executable_path()))
# Try different CMake generators since specifying no generator may fail
# in cygwin for some reason
@ -1741,41 +1684,6 @@ class CMakeDependency(ExternalDependency):
yield CMakeTraceLine(file, line, func, args)
def _reset_cmake_cache(self, build_dir):
with open('{}/CMakeCache.txt'.format(build_dir), 'w') as fp:
fp.write('CMAKE_PLATFORM_INFO_INITIALIZED:INTERNAL=1\n')
def _setup_compiler(self, build_dir):
comp_dir = '{}/CMakeFiles/{}'.format(build_dir, self.cmakevers)
os.makedirs(comp_dir, exist_ok=True)
c_comp = '{}/CMakeCCompiler.cmake'.format(comp_dir)
cxx_comp = '{}/CMakeCXXCompiler.cmake'.format(comp_dir)
if not os.path.exists(c_comp):
with open(c_comp, 'w') as fp:
fp.write('''# Fake CMake file to skip the boring and slow stuff
set(CMAKE_C_COMPILER "{}") # Just give CMake a valid full path to any file
set(CMAKE_C_COMPILER_ID "GNU") # Pretend we have found GCC
set(CMAKE_COMPILER_IS_GNUCC 1)
set(CMAKE_C_COMPILER_LOADED 1)
set(CMAKE_C_COMPILER_WORKS TRUE)
set(CMAKE_C_ABI_COMPILED TRUE)
set(CMAKE_SIZEOF_VOID_P "{}")
'''.format(os.path.realpath(__file__), ctypes.sizeof(ctypes.c_voidp)))
if not os.path.exists(cxx_comp):
with open(cxx_comp, 'w') as fp:
fp.write('''# Fake CMake file to skip the boring and slow stuff
set(CMAKE_CXX_COMPILER "{}") # Just give CMake a valid full path to any file
set(CMAKE_CXX_COMPILER_ID "GNU") # Pretend we have found GCC
set(CMAKE_COMPILER_IS_GNUCXX 1)
set(CMAKE_CXX_COMPILER_LOADED 1)
set(CMAKE_CXX_COMPILER_WORKS TRUE)
set(CMAKE_CXX_ABI_COMPILED TRUE)
set(CMAKE_SIZEOF_VOID_P "{}")
'''.format(os.path.realpath(__file__), ctypes.sizeof(ctypes.c_voidp)))
def _setup_cmake_dir(self, cmake_file: str) -> str:
# Setup the CMake build environment and return the "build" directory
build_dir = '{}/cmake_{}'.format(self.cmake_root_dir, self.name)
@ -1789,68 +1697,16 @@ set(CMAKE_SIZEOF_VOID_P "{}")
os.remove(cmake_lists)
shutil.copyfile(src_cmake, cmake_lists)
self._setup_compiler(build_dir)
self._reset_cmake_cache(build_dir)
return build_dir
def _call_cmake_real(self, args, cmake_file: str, env):
build_dir = self._setup_cmake_dir(cmake_file)
cmd = self.cmakebin.get_command() + args
p, out, err = Popen_safe(cmd, env=env, cwd=build_dir)
rc = p.returncode
call = ' '.join(cmd)
mlog.debug("Called `{}` in {} -> {}".format(call, build_dir, rc))
return rc, out, err
def _call_cmake(self, args, cmake_file: str, env=None):
if env is None:
fenv = env
env = os.environ
else:
fenv = frozenset(env.items())
targs = tuple(args)
# First check if cached, if not call the real cmake function
cache = CMakeDependency.cmake_cache
if (self.cmakebin, targs, cmake_file, fenv) not in cache:
cache[(self.cmakebin, targs, cmake_file, fenv)] = self._call_cmake_real(args, cmake_file, env)
return cache[(self.cmakebin, targs, cmake_file, fenv)]
build_dir = self._setup_cmake_dir(cmake_file)
return self.cmakebin.call_with_fake_build(args, build_dir, env=env)
@staticmethod
def get_methods():
return [DependencyMethods.CMAKE]
@staticmethod
def check_cmake(cmakebin):
if not cmakebin.found():
mlog.log('Did not find CMake {!r}'.format(cmakebin.name))
return None
try:
p, out = Popen_safe(cmakebin.get_command() + ['--version'])[0:2]
if p.returncode != 0:
mlog.warning('Found CMake {!r} but couldn\'t run it'
''.format(' '.join(cmakebin.get_command())))
return None
except FileNotFoundError:
mlog.warning('We thought we found CMake {!r} but now it\'s not there. How odd!'
''.format(' '.join(cmakebin.get_command())))
return None
except PermissionError:
msg = 'Found CMake {!r} but didn\'t have permissions to run it.'.format(' '.join(cmakebin.get_command()))
if not mesonlib.is_windows():
msg += '\n\nOn Unix-like systems this is often caused by scripts that are not executable.'
mlog.warning(msg)
return None
cmvers = re.sub(r'\s*cmake version\s*', '', out.split('\n')[0]).strip()
if not version_compare(cmvers, CMakeDependency.class_cmake_version):
mlog.warning(
'The version of CMake', mlog.bold(cmakebin.get_path()),
'is', mlog.bold(cmvers), 'but version', mlog.bold(CMakeDependency.class_cmake_version),
'is required')
return None
return cmvers
def log_tried(self):
return self.type_name