Skip to content

Commit

Permalink
adding diamond visits and avoid repetitions (#8701)
Browse files Browse the repository at this point in the history
  • Loading branch information
memsharded committed Mar 26, 2021
1 parent 0ad4f30 commit 306cfea
Show file tree
Hide file tree
Showing 5 changed files with 225 additions and 80 deletions.
19 changes: 11 additions & 8 deletions conan/tools/env/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,8 @@ def save_sh(self, filename, generate_deactivate=False, pathsep=os.pathsep):

def compose(self, other):
"""
self has precedence, the "other" will add/append if possible and not conflicting, but
self mandates what to do
:type other: Environment
"""
for k, v in other._values.items():
Expand All @@ -206,12 +208,12 @@ def compose(self, other):
self._values[k] = v
else:
try:
index = v.index(_EnvVarPlaceHolder)
except ValueError: # The other doesn't have placeholder, overwrites
self._values[k] = v
index = existing.index(_EnvVarPlaceHolder)
except ValueError: # It doesn't have placeholder
pass
else:
new_value = v[:] # do a copy
new_value[index:index + 1] = existing # replace the placeholder
new_value = existing[:] # do a copy
new_value[index:index + 1] = v # replace the placeholder
# Trim front and back separators
val = new_value[0]
if isinstance(val, _Sep) or val is _PathSep:
Expand All @@ -233,11 +235,12 @@ def __repr__(self):
def get_env(self, ref):
""" computes package-specific Environment
it is only called when conanfile.buildenv is called
the last one found in the profile file has top priority
"""
result = Environment()
for pattern, env in self._environments.items():
if pattern is None or fnmatch.fnmatch(str(ref), pattern):
result = result.compose(env)
result = env.compose(result)
return result

def compose(self, other):
Expand All @@ -247,7 +250,7 @@ def compose(self, other):
for pattern, environment in other._environments.items():
existing = self._environments.get(pattern)
if existing is not None:
self._environments[pattern] = existing.compose(environment)
self._environments[pattern] = environment.compose(existing)
else:
self._environments[pattern] = environment

Expand Down Expand Up @@ -283,7 +286,7 @@ def loads(text):
if existing is None:
result._environments[pattern] = env
else:
result._environments[pattern] = existing.compose(env)
result._environments[pattern] = env.compose(existing)
break
else:
raise ConanException("Bad env defintion: {}".format(line))
Expand Down
93 changes: 55 additions & 38 deletions conan/tools/env/virtualenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,32 +18,42 @@ def build_environment(self):
of build_requires defining information for consumers
"""
build_env = Environment()
# Top priority: profile
profile_env = self._conanfile.buildenv
build_env.compose(profile_env)

# First visit the direct build_requires
transitive_requires = []
for build_require in self._conanfile.dependencies.build_requires:
# Lower priority, the runenv of all transitive "requires" of the build requires
for require in build_require.dependencies.requires:
build_env.compose(self._collect_transitive_runenv(require))
# Second, the implicit self information in build_require.cpp_info
build_env.compose(self._runenv_from_cpp_info(build_require.cpp_info))
# Finally, higher priority, explicit buildenv_info
# higher priority, explicit buildenv_info
if build_require.buildenv_info:
build_env.compose(build_require.buildenv_info)
# Second, the implicit self information in build_require.cpp_info
build_env.compose(self._runenv_from_cpp_info(build_require.cpp_info))
# Lower priority, the runenv of all transitive "requires" of the build requires
for require in build_require.dependencies.requires:
if require not in transitive_requires:
transitive_requires.append(require)

self._apply_transitive_runenv(transitive_requires, build_env)

# Requires in host context can also bring some direct buildenv_info
def _collect_transitive_buildenv(d):
r = Environment()
for child in d.dependencies.requires:
r.compose(_collect_transitive_buildenv(child))
# Then the explicit self
if d.buildenv_info:
r.compose(d.buildenv_info)
return r
for require in self._conanfile.dependencies.requires:
build_env.compose(_collect_transitive_buildenv(require))

# The profile environment has precedence, applied last
profile_env = self._conanfile.buildenv
build_env.compose(profile_env)
def _apply_transitive_buildenv(reqs, env):
all_requires = []
while reqs:
new_requires = []
for r in reqs:
# The explicit has more priority
if r.buildenv_info:
env.compose(r.buildenv_info)
for transitive in r.dependencies.requires:
# Avoid duplication/repetitions
if transitive not in new_requires and transitive not in all_requires:
new_requires.append(transitive)
reqs = new_requires

_apply_transitive_buildenv(self._conanfile.dependencies.requires, build_env)

return build_env

@staticmethod
Expand All @@ -63,33 +73,40 @@ def _runenv_from_cpp_info(cpp_info):
dyn_runenv.prepend_path("DYLD_FRAMEWORK_PATH", cpp_info.framework_paths)
return dyn_runenv

def _collect_transitive_runenv(self, d):
r = Environment()
for child in d.dependencies.requires:
r.compose(self._collect_transitive_runenv(child))
# Apply "d" runenv, first the implicit
r.compose(self._runenv_from_cpp_info(d.cpp_info))
# Then the explicit
if d.runenv_info:
r.compose(d.runenv_info)
return r
def _apply_transitive_runenv(self, next_requires, runenv):
all_requires = []
while next_requires:
new_requires = []
for require in next_requires:
# The explicit has more priority
if require.runenv_info:
runenv.compose(require.runenv_info)
# Then the implicit
runenv.compose(self._runenv_from_cpp_info(require.cpp_info))
all_requires.append(require)

for transitive in require.dependencies.requires:
# Avoid duplication/repetitions
if transitive not in new_requires and transitive not in all_requires:
new_requires.append(transitive)
next_requires = new_requires

def run_environment(self):
""" collects the runtime information from dependencies. For normal libraries should be
very occasional
"""
runenv = Environment()
# FIXME: Missing profile info

# Visitor, breadth-first
self._apply_transitive_runenv(self._conanfile.dependencies.requires, runenv)
# At the moment we are adding "test-requires" (build_requires in host context)
# to the "runenv", but this will be investigated
for build_require in self._conanfile.dependencies.build_requires:
if build_require.context == CONTEXT_HOST:
runenv.compose(self._collect_transitive_runenv(build_require))
for require in self._conanfile.dependencies.requires:
runenv.compose(self._collect_transitive_runenv(require))
host_build_requires = [br for br in self._conanfile.dependencies.build_requires
if br.context == CONTEXT_HOST]
self._apply_transitive_runenv(host_build_requires, runenv)

# FIXME: Missing profile info
result = runenv
return result
return runenv

def generate(self):
build_env = self.build_environment()
Expand Down
12 changes: 12 additions & 0 deletions conans/model/conanfile_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,22 @@ class ConanFileInterface:
a limited view of conanfile dependencies, "read" only,
and only to some attributes, not methods
"""
def __str__(self):
return str(self._conanfile)

def __init__(self, conanfile):
self._conanfile = conanfile

def __eq__(self, other):
"""
The conanfile is a different entity per node, and conanfile equality is identity
:type other: ConanFileInterface
"""
return self._conanfile == other._conanfile

def __ne__(self, other):
return not self.__eq__(other)

@property
def buildenv_info(self):
return self._conanfile.buildenv_info
Expand Down
113 changes: 113 additions & 0 deletions conans/test/integration/environment/test_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,47 @@ def build(self):
assert "MYGTEST=Linux!!" in client.out


def test_profile_included_multiple():
client = TestClient()
conanfile = textwrap.dedent("""\
import os, platform
from conans import ConanFile
class Pkg(ConanFile):
def generate(self):
buildenv = self.buildenv
self.output.info("MYVAR1: {}!!!".format(buildenv.value("MYVAR1")))
self.output.info("MYVAR2: {}!!!".format(buildenv.value("MYVAR2")))
self.output.info("MYVAR3: {}!!!".format(buildenv.value("MYVAR3")))
""")

myprofile = textwrap.dedent("""
[buildenv]
MYVAR1=MyVal1
MYVAR3+=MyVal3
""")
other_profile = textwrap.dedent("""
[buildenv]
MYVAR1=MyValOther1
MYVAR2=MyValOther2
MYVAR3=MyValOther3
""")
client.save({"conanfile.py": conanfile,
"myprofile": myprofile,
"myprofile_include": "include(other_profile)\n" + myprofile,
"other_profile": other_profile})
# The reference profile has priority
client.run("install . -pr=myprofile_include")
assert "MYVAR1: MyVal1!!!" in client.out
assert "MYVAR2: MyValOther2!!!" in client.out
assert "MYVAR3: MyValOther3 MyVal3!!!" in client.out

# Equivalent to include is to put it first, then the last has priority
client.run("install . -pr=other_profile -pr=myprofile")
assert "MYVAR1: MyVal1!!!" in client.out
assert "MYVAR2: MyValOther2!!!" in client.out
assert "MYVAR3: MyValOther3 MyVal3!!!" in client.out


def test_profile_buildenv():
client = TestClient()
save(client.cache.new_config_path, "tools.env.virtualenv:auto_use=True")
Expand Down Expand Up @@ -269,3 +310,75 @@ def generate(self):
client.run("install . -s:b os=Windows -s:h os=Linux --build -g VirtualEnv")
assert "BUILDENV POCO: Poco_ROOT MyPocoLinuxValue!!!" in client.out
assert "BUILDENV OpenSSL: OpenSSL_ROOT MyOpenSSLLinuxValue!!!" in client.out


def test_diamond_repeated():
pkga = textwrap.dedent(r"""
from conans import ConanFile
class Pkg(ConanFile):
def package_info(self):
self.runenv_info.define("MYVAR1", "PkgAValue1")
self.runenv_info.append("MYVAR2", "PkgAValue2")
self.runenv_info.prepend("MYVAR3", "PkgAValue3")
self.runenv_info.prepend("MYVAR4", "PkgAValue4")
""")
pkgb = textwrap.dedent(r"""
from conans import ConanFile
class Pkg(ConanFile):
requires = "pkga/1.0"
def package_info(self):
self.runenv_info.append("MYVAR1", "PkgBValue1")
self.runenv_info.append("MYVAR2", "PkgBValue2")
self.runenv_info.prepend("MYVAR3", "PkgBValue3")
self.runenv_info.prepend("MYVAR4", "PkgBValue4")
""")
pkgc = textwrap.dedent(r"""
from conans import ConanFile
class Pkg(ConanFile):
requires = "pkga/1.0"
def package_info(self):
self.runenv_info.append("MYVAR1", "PkgCValue1")
self.runenv_info.append("MYVAR2", "PkgCValue2")
self.runenv_info.prepend("MYVAR3", "PkgCValue3")
self.runenv_info.prepend("MYVAR4", "PkgCValue4")
""")
pkgd = textwrap.dedent(r"""
from conans import ConanFile
class Pkg(ConanFile):
requires = "pkgb/1.0", "pkgc/1.0"
def package_info(self):
self.runenv_info.append("MYVAR1", "PkgDValue1")
self.runenv_info.append("MYVAR2", "PkgDValue2")
self.runenv_info.prepend("MYVAR3", "PkgDValue3")
self.runenv_info.define("MYVAR4", "PkgDValue4")
""")
pkge = textwrap.dedent(r"""
from conans import ConanFile
from conan.tools.env import VirtualEnv
class Pkg(ConanFile):
requires = "pkgd/1.0"
def generate(self):
env = VirtualEnv(self)
runenv = env.run_environment()
self.output.info("MYVAR1: {}!!!".format(runenv.value("MYVAR1")))
self.output.info("MYVAR2: {}!!!".format(runenv.value("MYVAR2")))
self.output.info("MYVAR3: {}!!!".format(runenv.value("MYVAR3")))
self.output.info("MYVAR4: {}!!!".format(runenv.value("MYVAR4")))
""")
client = TestClient()
client.save({"pkga/conanfile.py": pkga,
"pkgb/conanfile.py": pkgb,
"pkgc/conanfile.py": pkgc,
"pkgd/conanfile.py": pkgd,
"pkge/conanfile.py": pkge})

client.run("export pkga pkga/1.0@")
client.run("export pkgb pkgb/1.0@")
client.run("export pkgc pkgc/1.0@")
client.run("export pkgd pkgd/1.0@")

client.run("install pkge --build")
assert "MYVAR1: PkgAValue1 PkgCValue1 PkgBValue1 PkgDValue1!!!" in client.out
assert "MYVAR2: MYVAR2 PkgAValue2 PkgCValue2 PkgBValue2 PkgDValue2!!!" in client.out
assert "MYVAR3: PkgDValue3 PkgBValue3 PkgCValue3 PkgAValue3 MYVAR3!!!" in client.out
assert "MYVAR4: PkgDValue4!!!" in client.out

0 comments on commit 306cfea

Please sign in to comment.