1import os
2import platform
3from contextlib import contextmanager
4
5import six
6from six import string_types
7
8
9from conans.client import tools
10from conans.client.output import ScopedOutput
11from conans.client.tools.env import environment_append, no_op, pythonpath
12from conans.client.tools.oss import OSInfo
13from conans.errors import ConanException, ConanInvalidConfiguration
14from conans.model.build_info import DepsCppInfo
15from conans.model.conf import Conf
16from conans.model.dependencies import ConanFileDependencies
17from conans.model.env_info import DepsEnvInfo
18from conans.model.layout import Folders, Infos
19from conans.model.new_build_info import from_old_cppinfo
20from conans.model.options import Options, OptionsValues, PackageOptions
21from conans.model.requires import Requirements
22from conans.model.user_info import DepsUserInfo
23from conans.paths import RUN_LOG_NAME
24from conans.util.conan_v2_mode import conan_v2_error
25
26
27def create_options(conanfile):
28    try:
29        package_options = PackageOptions(getattr(conanfile, "options", None))
30        options = Options(package_options)
31
32        default_options = getattr(conanfile, "default_options", None)
33        if default_options:
34            if isinstance(default_options, dict):
35                default_values = OptionsValues(default_options)
36            elif isinstance(default_options, (list, tuple)):
37                conan_v2_error("Declare 'default_options' as a dictionary")
38                default_values = OptionsValues(default_options)
39            elif isinstance(default_options, six.string_types):
40                conan_v2_error("Declare 'default_options' as a dictionary")
41                default_values = OptionsValues.loads(default_options)
42            else:
43                raise ConanException("Please define your default_options as list, "
44                                     "multiline string or dictionary")
45            options.values = default_values
46        return options
47    except Exception as e:
48        raise ConanException("Error while initializing options. %s" % str(e))
49
50
51def create_requirements(conanfile):
52    try:
53        # Actual requirements of this package
54        if not hasattr(conanfile, "requires"):
55            return Requirements()
56        else:
57            if not conanfile.requires:
58                return Requirements()
59            if isinstance(conanfile.requires, (tuple, list)):
60                return Requirements(*conanfile.requires)
61            else:
62                return Requirements(conanfile.requires, )
63    except Exception as e:
64        raise ConanException("Error while initializing requirements. %s" % str(e))
65
66
67def create_settings(conanfile, settings):
68    try:
69        defined_settings = getattr(conanfile, "settings", None)
70        if isinstance(defined_settings, str):
71            defined_settings = [defined_settings]
72        current = defined_settings or {}
73        settings.constraint(current)
74        return settings
75    except Exception as e:
76        raise ConanInvalidConfiguration("The recipe %s is constraining settings. %s" % (
77                                        conanfile.display_name, str(e)))
78
79
80@contextmanager
81def _env_and_python(conanfile):
82    with environment_append(conanfile.env):
83        # FIXME Conan 2.0, Remove old ways of reusing python code
84        with pythonpath(conanfile):
85            yield
86
87
88def get_env_context_manager(conanfile, without_python=False):
89    if not conanfile.apply_env:
90        return no_op()
91    if without_python:
92        return environment_append(conanfile.env)
93    return _env_and_python(conanfile)
94
95
96class ConanFile(object):
97    """ The base class for all package recipes
98    """
99
100    name = None
101    version = None  # Any str, can be "1.1" or whatever
102    url = None  # The URL where this File is located, as github, to collaborate in package
103    # The license of the PACKAGE, just a shortcut, does not replace or
104    # change the actual license of the source code
105    license = None
106    author = None  # Main maintainer/responsible for the package, any format
107    description = None
108    topics = None
109    homepage = None
110    build_policy = None
111    short_paths = False
112    apply_env = True  # Apply environment variables from requires deps_env_info and profiles
113    exports = None
114    exports_sources = None
115    generators = ["txt"]
116    revision_mode = "hash"
117
118    # Vars to control the build steps (build(), package())
119    should_configure = True
120    should_build = True
121    should_install = True
122    should_test = True
123    in_local_cache = True
124    develop = False
125
126    # Defaulting the reference fields
127    default_channel = None
128    default_user = None
129
130    # Settings and Options
131    settings = None
132    options = None
133    default_options = None
134
135    provides = None
136    deprecated = None
137
138    # Folders
139    folders = None
140    patterns = None
141
142    # Run in windows bash
143    win_bash = None
144
145    def __init__(self, output, runner, display_name="", user=None, channel=None):
146        # an output stream (writeln, info, warn error)
147        self.output = ScopedOutput(display_name, output)
148        self.display_name = display_name
149        # something that can run commands, as os.sytem
150        self._conan_runner = runner
151        self._conan_user = user
152        self._conan_channel = channel
153
154        self.compatible_packages = []
155        self._conan_using_build_profile = False
156        self._conan_requester = None
157        from conan.tools.env import Environment
158        self.buildenv_info = Environment()
159        self.runenv_info = Environment()
160        # At the moment only for build_requires, others will be ignored
161        self.conf_info = Conf()
162        self._conan_buildenv = None  # The profile buildenv, will be assigned initialize()
163        self._conan_node = None  # access to container Node object, to access info, context, deps...
164        self._conan_new_cpp_info = None   # Will be calculated lazy in the getter
165        self._conan_dependencies = None
166
167        self.env_scripts = {}  # Accumulate the env scripts generated in order
168
169        # layout() method related variables:
170        self.folders = Folders()
171        self.cpp = Infos()
172
173        self.cpp.package.includedirs = ["include"]
174        self.cpp.package.libdirs = ["lib"]
175        self.cpp.package.bindirs = ["bin"]
176        self.cpp.package.resdirs = ["res"]
177        self.cpp.package.builddirs = [""]
178        self.cpp.package.frameworkdirs = ["Frameworks"]
179
180    @property
181    def context(self):
182        return self._conan_node.context
183
184    @property
185    def dependencies(self):
186        # Caching it, this object is requested many times
187        if self._conan_dependencies is None:
188            self._conan_dependencies = ConanFileDependencies.from_node(self._conan_node)
189        return self._conan_dependencies
190
191    @property
192    def ref(self):
193        return self._conan_node.ref
194
195    @property
196    def pref(self):
197        return self._conan_node.pref
198
199    @property
200    def buildenv(self):
201        # Lazy computation of the package buildenv based on the profileone
202        from conan.tools.env import Environment
203        if not isinstance(self._conan_buildenv, Environment):
204            # TODO: missing user/channel
205            ref_str = "{}/{}".format(self.name, self.version)
206            self._conan_buildenv = self._conan_buildenv.get_profile_env(ref_str)
207        return self._conan_buildenv
208
209    def initialize(self, settings, env, buildenv=None):
210        self._conan_buildenv = buildenv
211        if isinstance(self.generators, str):
212            self.generators = [self.generators]
213        # User defined options
214        self.options = create_options(self)
215        self.requires = create_requirements(self)
216        self.settings = create_settings(self, settings)
217
218        conan_v2_error("Setting 'cppstd' is deprecated in favor of 'compiler.cppstd',"
219                       " please update your recipe.", 'cppstd' in self.settings.fields)
220
221        # needed variables to pack the project
222        self.cpp_info = None  # Will be initialized at processing time
223        self._conan_dep_cpp_info = None  # Will be initialized at processing time
224        self.deps_cpp_info = DepsCppInfo()
225
226        # environment variables declared in the package_info
227        self.env_info = None  # Will be initialized at processing time
228        self.deps_env_info = DepsEnvInfo()
229
230        # user declared variables
231        self.user_info = None
232        # Keys are the package names (only 'host' if different contexts)
233        self.deps_user_info = DepsUserInfo()
234
235        # user specified env variables
236        self._conan_env_values = env.copy()  # user specified -e
237
238        if self.description is not None and not isinstance(self.description, six.string_types):
239            raise ConanException("Recipe 'description' must be a string.")
240
241        if not hasattr(self, "virtualbuildenv"):  # Allow the user to override it with True or False
242            self.virtualbuildenv = True
243        if not hasattr(self, "virtualrunenv"):  # Allow the user to override it with True or False
244            self.virtualrunenv = True
245
246    @property
247    def new_cpp_info(self):
248        if not self._conan_new_cpp_info:
249            self._conan_new_cpp_info = from_old_cppinfo(self.cpp_info)
250        return self._conan_new_cpp_info
251
252    @property
253    def source_folder(self):
254        return self.folders.source_folder
255
256    @property
257    def build_folder(self):
258        return self.folders.build_folder
259
260    @property
261    def package_folder(self):
262        return self.folders.base_package
263
264    @property
265    def install_folder(self):
266        # FIXME: Remove in 2.0, no self.install_folder
267        return self.folders.base_install
268
269    @property
270    def generators_folder(self):
271        # FIXME: Remove in 2.0, no self.install_folder
272        return self.folders.generators_folder if self.folders.generators else self.install_folder
273
274    @property
275    def imports_folder(self):
276        return self.folders.imports_folder
277
278    @property
279    def env(self):
280        """Apply the self.deps_env_info into a copy of self._conan_env_values (will prioritize the
281        self._conan_env_values, user specified from profiles or -e first, then inherited)"""
282        # Cannot be lazy cached, because it's called in configure node, and we still don't have
283        # the deps_env_info objects available
284        tmp_env_values = self._conan_env_values.copy()
285        tmp_env_values.update(self.deps_env_info)
286        ret, multiple = tmp_env_values.env_dicts(self.name, self.version, self._conan_user,
287                                                 self._conan_channel)
288        ret.update(multiple)
289        return ret
290
291    @property
292    def channel(self):
293        if not self._conan_channel:
294            _env_channel = os.getenv("CONAN_CHANNEL")
295            conan_v2_error("Environment variable 'CONAN_CHANNEL' is deprecated", _env_channel)
296            self._conan_channel = _env_channel or self.default_channel
297            if not self._conan_channel:
298                raise ConanException("channel not defined, but self.channel is used in conanfile")
299        return self._conan_channel
300
301    @property
302    def user(self):
303        if not self._conan_user:
304            _env_username = os.getenv("CONAN_USERNAME")
305            conan_v2_error("Environment variable 'CONAN_USERNAME' is deprecated", _env_username)
306            self._conan_user = _env_username or self.default_user
307            if not self._conan_user:
308                raise ConanException("user not defined, but self.user is used in conanfile")
309        return self._conan_user
310
311    def collect_libs(self, folder=None):
312        conan_v2_error("'self.collect_libs' is deprecated, use 'tools.collect_libs(self)' instead")
313        return tools.collect_libs(self, folder=folder)
314
315    @property
316    def build_policy_missing(self):
317        return self.build_policy == "missing"
318
319    @property
320    def build_policy_always(self):
321        return self.build_policy == "always"
322
323    def source(self):
324        pass
325
326    def system_requirements(self):
327        """ this method can be overwritten to implement logic for system package
328        managers, as apt-get
329
330        You can define self.global_system_requirements = True, if you want the installation
331        to be for all packages (not depending on settings/options/requirements)
332        """
333
334    def config_options(self):
335        """ modify options, probably conditioned to some settings. This call is executed
336        before config_settings. E.g.
337        if self.settings.os == "Windows":
338            del self.options.shared  # shared/static not supported in win
339        """
340
341    def configure(self):
342        """ modify settings, probably conditioned to some options. This call is executed
343        after config_options. E.g.
344        if self.options.header_only:
345            self.settings.clear()
346        This is also the place for conditional requirements
347        """
348
349    def build(self):
350        """ build your project calling the desired build tools as done in the command line.
351        E.g. self.run("cmake --build .") Or use the provided build helpers. E.g. cmake.build()
352        """
353        self.output.warn("This conanfile has no build step")
354
355    def package(self):
356        """ package the needed files from source and build folders.
357        E.g. self.copy("*.h", src="src/includes", dst="includes")
358        """
359        self.output.warn("This conanfile has no package step")
360
361    def package_info(self):
362        """ define cpp_build_info, flags, etc
363        """
364
365    def run(self, command, output=True, cwd=None, win_bash=False, subsystem=None, msys_mingw=True,
366            ignore_errors=False, run_environment=False, with_login=True, env=None):
367        # NOTE: "self.win_bash" is the new parameter "win_bash" for Conan 2.0
368
369        def _run(cmd, _env):
370            # FIXME: run in windows bash is not using output
371            if platform.system() == "Windows":
372                if win_bash:
373                    return tools.run_in_windows_bash(self, bashcmd=cmd, cwd=cwd, subsystem=subsystem,
374                                                     msys_mingw=msys_mingw, with_login=with_login)
375                elif self.win_bash:  # New, Conan 2.0
376                    from conan.tools.microsoft.subsystems import run_in_windows_bash
377                    return run_in_windows_bash(self, command=cmd, cwd=cwd, env=_env)
378            if _env is None:
379                _env = "conanbuild"
380            from conan.tools.env.environment import environment_wrap_command
381            wrapped_cmd = environment_wrap_command(_env, cmd, cwd=self.generators_folder)
382            return self._conan_runner(wrapped_cmd, output, os.path.abspath(RUN_LOG_NAME), cwd)
383
384        if run_environment:
385            # When using_build_profile the required environment is already applied through
386            # 'conanfile.env' in the contextmanager 'get_env_context_manager'
387            with tools.run_environment(self) if not self._conan_using_build_profile else no_op():
388                if OSInfo().is_macos and isinstance(command, string_types):
389                    # Security policy on macOS clears this variable when executing /bin/sh. To
390                    # keep its value, set it again inside the shell when running the command.
391                    command = 'DYLD_LIBRARY_PATH="%s" DYLD_FRAMEWORK_PATH="%s" %s' % \
392                              (os.environ.get('DYLD_LIBRARY_PATH', ''),
393                               os.environ.get("DYLD_FRAMEWORK_PATH", ''),
394                               command)
395                retcode = _run(command, env)
396        else:
397            retcode = _run(command, env)
398
399        if not ignore_errors and retcode != 0:
400            raise ConanException("Error %d while executing %s" % (retcode, command))
401
402        return retcode
403
404    def package_id(self):
405        """ modify the binary info, typically to narrow values
406        e.g.: self.info.settings.compiler = "Any" => All compilers will generate same ID
407        """
408
409    def test(self):
410        """ test the generated executable.
411        E.g.  self.run("./example")
412        """
413        raise ConanException("You need to create a method 'test' in your test/conanfile.py")
414
415    def __repr__(self):
416        return self.display_name
417