Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion doc/manual/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1178,6 +1178,7 @@ The following settings are supported:
| Name | Type | Description |
+=============+=================+=====================================================+
| name | String | The name of the required recipe. |
| | | String substitution is applied to this setting. |
+-------------+-----------------+-----------------------------------------------------+
| depends | List of | A list of dependencies inheriting the settings of |
| | Dependencies | this entry. |
Expand Down Expand Up @@ -1567,7 +1568,8 @@ The ``provideDeps`` keyword receives a list of dependency names. These must be
dependencies of the current recipe, i.e. they must appear in the ``depends``
section. It is no error if the condition of such a dependency evaluates to
false. In this case the entry is silently dropped. To specify multiple
dependencies with a single entry shell globbing patterns may be used.
dependencies with a single entry shell globbing patterns may be used. As for the
names of the dependencies string substitution is also applied to ``provideDeps``.

Provided dependencies are subsequently injected into the dependency list of the
downstream recipe that has a dependency to this one (if ``deps`` is included in
Expand Down
82 changes: 63 additions & 19 deletions pym/bob/input.py
Original file line number Diff line number Diff line change
Expand Up @@ -1849,6 +1849,40 @@ def useResultOnce(self):
self.usedResult = True
return True


class VerbatimProvideDepsResolver:
def __init__(self, pattern):
self.pattern = pattern

def resolve(self, env, resolvedDeps):
pattern = self.pattern
return set(d for d in resolvedDeps if d == pattern)

class GlobProvideDepsResolver:
def __init__(self, pattern):
self.pattern = pattern

def resolve(self, env, resolvedDeps):
pattern = self.pattern
return set(d for d in resolvedDeps if fnmatch.fnmatchcase(d, pattern))

class SubstituteProvideDepsResolver:
def __init__(self, pattern):
self.pattern = pattern

def resolve(self, env, resolvedDeps):
pattern = self.pattern
pattern = env.substitute(pattern, "providedDeps::"+pattern)
return set(d for d in resolvedDeps if fnmatch.fnmatchcase(d, pattern))

def getProvideDepsResolver(pattern):
if any((c in pattern) for c in '\\\"\'$'):
return SubstituteProvideDepsResolver(pattern)
elif any((c in pattern) for c in '*?['):
return GlobProvideDepsResolver(pattern)
else:
return VerbatimProvideDepsResolver(pattern)

class Recipe(object):
"""Representation of a single recipe

Expand Down Expand Up @@ -2220,15 +2254,8 @@ def coDet(r):
if self.__jobServer is None:
self.__jobServer = False

# check provided dependencies
availDeps = [ d.recipe for d in self.__deps ]
providedDeps = set()
for pattern in self.__provideDeps:
l = set(d for d in availDeps if fnmatch.fnmatchcase(d, pattern))
if not l:
raise ParseError("Unknown dependency '{}' in provideDeps".format(pattern))
providedDeps |= l
self.__provideDeps = providedDeps
# Optimize provideDeps
self.__provideDeps = [ getProvideDepsResolver(d) for d in self.__provideDeps ]

# Evaluate root property
if isinstance(self.__root, str) or isinstance(self.__root, IfExpression):
Expand Down Expand Up @@ -2346,6 +2373,7 @@ def prepare(self, inputEnv, sandboxEnabled, inputStates, inputSandbox=None,
directPackages = []
indirectPackages = []
provideDeps = UniquePackageList(stack, self.__raiseIncompatibleProvided)
maybeProvideDeps = []
checkoutDeps = []
results = []
depEnv = env.derive()
Expand All @@ -2355,12 +2383,16 @@ def prepare(self, inputEnv, sandboxEnabled, inputStates, inputSandbox=None,
depDiffSandbox = diffSandbox
depDiffTools = diffTools.copy()
thisDeps = {}
resolvedDeps = []

for dep in self.__deps:
env.setFunArgs({ "recipe" : self, "sandbox" : bool(sandbox) and sandboxEnabled,
"__tools" : tools })

if dep.condition and not all(env.evaluate(cond, "dependency "+dep.recipe)
recipe = env.substitute(dep.recipe, "dependency::"+dep.recipe)
resolvedDeps.append(recipe)

if dep.condition and not all(env.evaluate(cond, "dependency "+recipe)
for cond in dep.condition): continue

if dep.toolOverride:
Expand All @@ -2369,7 +2401,7 @@ def prepare(self, inputEnv, sandboxEnabled, inputStates, inputSandbox=None,
k : depTools[v] for k,v in dep.toolOverride.items() })
except KeyError as e:
raise ParseError("Cannot remap unkown tool '{}' for dependency '{}'!"
.format(e.args[0], dep.recipe))
.format(e.args[0], recipe))
thisDepDiffTools = depDiffTools.copy()
thisDepDiffTools.update({
k : depDiffTools.get(v, v)
Expand All @@ -2379,10 +2411,10 @@ def prepare(self, inputEnv, sandboxEnabled, inputStates, inputSandbox=None,
thisDepDiffTools = depDiffTools

thisDepEnv = depEnv.derive(
{ key : env.substitute(value, "depends["+dep.recipe+"].environment["+key+"]")
{ key : env.substitute(value, "depends["+recipe+"].environment["+key+"]")
for key, value in dep.envOverride.items() })

r = self.__recipeSet.getRecipe(dep.recipe)
r = self.__recipeSet.getRecipe(recipe)
try:
if r.__packageName in stack:
raise ParseError("Recipes are cyclic (1st package in cylce)")
Expand All @@ -2400,16 +2432,16 @@ def prepare(self, inputEnv, sandboxEnabled, inputStates, inputSandbox=None,
# A dependency should be named only once. Hence we can
# optimistically create the DepTracker object. If the dependency is
# named more than one we make sure that it is the same variant.
depTrack = thisDeps.setdefault(dep.recipe, DepTracker(depRef))
depTrack = thisDeps.setdefault(recipe, DepTracker(depRef))
if depTrack.prime():
directPackages.append(depRef)
elif depCoreStep.variantId != depTrack.item.refGetDestination().variantId:
self.__raiseIncompatibleLocal(depCoreStep)
elif self.__recipeSet.getPolicy('uniqueDependency'):
raise ParseError("Duplicate dependency '{}'. Each dependency must only be named once!"
.format(dep.recipe))
.format(recipe))
else:
warnDepends.show("{} -> {}".format(self.__packageName, dep.recipe))
warnDepends.show("{} -> {}".format(self.__packageName, recipe))

# Remember dependency diffs before changing them
origDepDiffTools = thisDepDiffTools
Expand Down Expand Up @@ -2449,10 +2481,22 @@ def prepare(self, inputEnv, sandboxEnabled, inputStates, inputSandbox=None,
if sandboxEnabled:
env.update(sandbox.environment)
if dep.provideGlobal: depEnv.update(sandbox.environment)
if dep.recipe in self.__provideDeps:

maybeProvideDeps.append((recipe, depRef, p.getName(), origDepDiffTools, origDepDiffSandbox))

# check provided dependencies
providedDeps = set()
for pattern in self.__provideDeps:
l = pattern.resolve(env, resolvedDeps)
if not l:
raise ParseError("Unknown dependency '{}' in provideDeps".format(pattern.pattern))
providedDeps |= l

for (recipe, depRef, name, origDepDiffTools, origDepDiffSandbox) in maybeProvideDeps:
if recipe in providedDeps:
provideDeps.append(depRef)
provideDeps.extend(CoreRef(d, [p.getName()], origDepDiffTools, origDepDiffSandbox)
for d in depCoreStep.providedDeps)
provideDeps.extend([CoreRef(d, [name], origDepDiffTools, origDepDiffSandbox)
for d in depRef.refGetDestination().providedDeps])

# Filter indirect packages and add to result list if necessary. Most
# likely there are many duplicates that are dropped.
Expand Down
95 changes: 95 additions & 0 deletions test/unit/test_input_recipeset.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,101 @@ def testInvalidMinVerStr(self):


class TestDependencies(RecipesTmp, TestCase):
def testVariableDeps(self):
"""Test resolve of dependecies by environment substitution"""
self.writeRecipe("root", """\
root: True
depends: [a]
environment:
A : "b"
D : "c"
buildScript: "true"
packageScript: "true"
""")
self.writeRecipe("root2", """\
root: True
depends: [a, d]
environment:
A : "c"
buildScript: "true"
packageScript: "true"
""")

self.writeRecipe("a", """\
depends: [ "$A-foo" ]
buildScript: "true"
packageScript: "true"
provideDeps: [ "$A-f*" ]
provideVars:
D: "e"
""")

self.writeRecipe("b-foo", """\
buildScript: "true"
packageScript: "echo 'b'"
""")
self.writeRecipe("c-foo", """\
buildScript: "true"
packageScript: "echo 'c'"
""")
self.writeRecipe("d", """\
depends:
- name: a
use: [environment, deps]
- "$D"
buildScript: "true"
packageScript: "true"
""")
self.writeRecipe("e", """\
buildScript: "true"
packageScript: "true"
""")

recipes = RecipeSet()
recipes.parse()
packages = recipes.generatePackages(lambda x,y: "unused")

p = packages.walkPackagePath("root/a/b-foo")
self.assertEqual(p.getName(), "b-foo")
p = packages.walkPackagePath("root2/a/c-foo")
self.assertEqual(p.getName(), "c-foo")
p = packages.walkPackagePath("root2/d/e")
self.assertEqual(p.getName(), "e")
#access via providedDeps
p = packages.walkPackagePath("root/b-foo")
self.assertEqual(p.getName(), "b-foo")

def testGlobProvideDeps(self):
"""Test globbing pattern in provideDeps"""
self.writeRecipe("root", """\
root: True
depends: [a]
buildScript: "true"
packageScript: "true"
""")
self.writeRecipe("a", """\
depends: [b-dev, b-tgt]
packageScript: "echo a"
provideDeps: [ "*-dev" ]
""")
self.writeRecipe("b", """\
multiPackage:
dev:
packageScript: "echo b-dev"
tgt:
packageScript: "echo b-tgt"
""")

recipes = RecipeSet()
recipes.parse()
packages = recipes.generatePackages(lambda x,y: "unused")

rootArgs = packages.walkPackagePath("root").getBuildStep().getArguments()
self.assertEqual(len(rootArgs), 3)
self.assertEqual(rootArgs[0].getPackage().getName(), "root")
self.assertEqual(rootArgs[1].getPackage().getName(), "a")
self.assertEqual(rootArgs[2].getPackage().getName(), "b-dev")

def testDuplicateRemoval(self):
"""Test that provided dependencies do not replace real dependencies"""
self.writeRecipe("root", """\
Expand Down