Skip to content

Commit e3644bb

Browse files
authored
Merge pull request #596 from jkloetzke/layer-compat
Restore pre-0.25 nested layers compatibility
2 parents b014848 + d4dad4b commit e3644bb

File tree

9 files changed

+159
-36
lines changed

9 files changed

+159
-36
lines changed

doc/manual/configuration.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1968,6 +1968,10 @@ possibility is to provide an SCM-Dictionary (see
19681968
commit: ...
19691969
- bsp
19701970

1971+
.. note::
1972+
Managed layers are only supported if the :ref:`policies-managedLayers`
1973+
policy is set to the new behaviour.
1974+
19711975
If a layer SCM specification is given, Bob takes care of the layer management:
19721976

19731977
- Layers are checked out / updated during bob-build (except build-only).

doc/manual/policies.rst

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,60 @@ Old behavior
290290
New behavior
291291
Apply string substitution to ``metaEnvironment`` variables.
292292

293+
.. _policies-managedLayers:
294+
295+
managedLayers
296+
~~~~~~~~~~~~~
297+
298+
Introduced in: 0.25
299+
300+
Starting with Bob version 0.25, managed layers are supported. This changed the
301+
location where layers are stored, though. Historically, layers could be nested
302+
where they would form a tree structure. That is, each layer can have a ``layers``
303+
directory itself where further layers are located. Because this does not work
304+
if multiple layers refer to another common layer, the directory structure
305+
has been flattened.
306+
307+
Old behavior
308+
Keep support for projects that were created before Bob 0.25. Layers with
309+
sub-layers form a tree structure. See the following example::
310+
311+
.
312+
├── config.yaml
313+
├── layers
314+
│   └── foo
315+
│   ├── config.yaml
316+
│   ├── layers
317+
│   │   ├── bar
318+
│   │   │   └── recipes
319+
│   │   └── baz
320+
│   │   └── recipes
321+
│   └── recipes
322+
└── recipes
323+
324+
No SCM can be used in the :ref:`configuration-config-layers` section of
325+
``config.yaml``. The :ref:`manpage-layers` command will refuse to work on
326+
such projectes.
327+
328+
New behavior
329+
Managed layers are supported, that is SCMs can be used in the
330+
:ref:`configuration-config-layers` section of ``config.yaml``. The layers
331+
are checked out flat into the ``layers`` directory of the project::
332+
333+
.
334+
├── config.yaml
335+
├── layers
336+
│   ├── bar
337+
│   │   └── recipes
338+
│   ├── baz
339+
│   │   └── recipes
340+
│   └── foo
341+
│   ├── config.yaml
342+
│   └── recipes
343+
└── recipes
344+
345+
Unmanaged layers are expected in the same directory.
346+
293347
.. _policies-obsolete:
294348

295349
Obsolete policies

pym/bob/builder.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1479,7 +1479,7 @@ async def _downloadPackage(self, packageStep, depth, packageBuildId):
14791479
* still same build-id -> done
14801480
* build-id changed -> prune and try download, fall back to build
14811481
"""
1482-
layer = packageStep.getPackage().getRecipe().getLayer()
1482+
layer = "/".join(packageStep.getPackage().getRecipe().getLayer())
14831483
layerDownloadMode = None
14841484
if layer:
14851485
for mode in self.__downloadLayerModes:

pym/bob/cmds/build/build.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ def _downloadLayerArgument(arg):
238238
recipes.setConfigFiles(args.configFile)
239239
if args.build_mode != 'build-only':
240240
setVerbosity(args.verbose)
241-
updateLayers(loop, defines, args.verbose, args.attic, args.layerConfig)
241+
updateLayers(loop, defines, args.verbose, args.attic, args.layerConfig, False)
242242
recipes.parse(defines)
243243

244244
# if arguments are not passed on cmdline use them from default.yaml or set to default yalue

pym/bob/input.py

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2087,9 +2087,16 @@ def getLayer(self):
20872087
are named from top to bottom. Example:
20882088
``layers/foo/layers/bar/recipes/baz.yaml`` -> ``['foo', 'bar']``.
20892089
2090+
If the managedLayers policy is set to the new behaviour, nested layers
2091+
are flattened. This means that layers are always returnd as single-item
2092+
lists.
2093+
20902094
:rtype: List[str]
20912095
"""
2092-
return self.__layer
2096+
if self.__layer:
2097+
return self.__layer.split("/")
2098+
else:
2099+
return []
20932100

20942101
def resolveClasses(self, rootEnv):
20952102
# must be done only once
@@ -3003,6 +3010,7 @@ class RecipeSet:
30033010
schema.Optional('fixImportScmVariant') : bool,
30043011
schema.Optional('defaultFileMode') : bool,
30053012
schema.Optional('substituteMetaEnv') : bool,
3013+
schema.Optional('managedLayers') : bool,
30063014
},
30073015
error="Invalid policy specified! Are you using an appropriate version of Bob?"
30083016
),
@@ -3051,7 +3059,12 @@ class RecipeSet:
30513059
"0.25rc1",
30523060
InfoOnce("substituteMetaEnv policy is not set. MetaEnv will not be substituted.",
30533061
help="See http://bob-build-tool.readthedocs.io/en/latest/manual/policies.html#substitutemetaenv for more information.")
3054-
)
3062+
),
3063+
"managedLayers": (
3064+
"0.25.0rc2.dev6",
3065+
InfoOnce("managedLayers policy is not set. Only unmanaged layers are supported.",
3066+
help="See http://bob-build-tool.readthedocs.io/en/latest/manual/policies.html#managedlayers for more information.")
3067+
),
30553068
}
30563069

30573070
_ignoreCmdConfig = False
@@ -3481,7 +3494,7 @@ def __parse(self, envOverrides, platform, recipesRoot=""):
34813494
os.path.join(os.path.expanduser("~"), '.config')), 'bob', 'default.yaml'))
34823495

34833496
# Begin with root layer
3484-
self.__parseLayer(LayerSpec(""), "9999", recipesRoot)
3497+
self.__parseLayer(LayerSpec(""), "9999", recipesRoot, None)
34853498

34863499
# Out-of-tree builds may have a dedicated default.yaml
34873500
if recipesRoot:
@@ -3557,20 +3570,40 @@ def calculatePolicies(cls, config):
35573570
ret[name] = (behaviour, None)
35583571
return ret
35593572

3560-
def __parseLayer(self, layerSpec, maxVer, recipesRoot):
3573+
def __parseLayer(self, layerSpec, maxVer, recipesRoot, upperLayer):
35613574
layer = layerSpec.getName()
3562-
3563-
if layer in self.__layers:
3564-
return
3565-
self.__layers.append(layer)
3566-
3567-
# SCM backed layers are in build dir, regular layers are in project dir.
3568-
rootDir = recipesRoot if layerSpec.getScm() is None else ""
35693575
if layer:
3570-
rootDir = os.path.join(rootDir, "layers", layer)
3571-
if not os.path.isdir(rootDir or "."):
3572-
raise ParseError(f"Layer '{layer}' does not exist!",
3576+
# Managed layers imply that layers are potentially nested instead
3577+
# of being checked out next to each other in the build directory.
3578+
managedLayers = self.getPolicy('managedLayers')
3579+
if layerSpec.getScm() is not None and not managedLayers:
3580+
raise ParseError("Managed layers aren't enabled! See the managedLayers policy for details.")
3581+
3582+
# Pre 0.25, layers could be nested.
3583+
if not managedLayers and upperLayer:
3584+
layer = upperLayer + "/" + layer
3585+
3586+
if layer in self.__layers:
3587+
return
3588+
self.__layers.append(layer)
3589+
3590+
if managedLayers:
3591+
# SCM backed layers are in build dir, regular layers are in
3592+
# project dir.
3593+
rootDir = recipesRoot if layerSpec.getScm() is None else ""
3594+
rootDir = os.path.join(rootDir, "layers", layer)
3595+
if not os.path.isdir(rootDir):
3596+
raise ParseError(f"Layer '{layer}' does not exist!",
35733597
help="You probably want to run 'bob layers update' to fetch missing layers.")
3598+
else:
3599+
# Before managed layers existed, layers could be nested in the
3600+
# project directory.
3601+
rootDir = os.path.join(recipesRoot, *( os.path.join("layers", l)
3602+
for l in layer.split("/") ))
3603+
if not os.path.isdir(rootDir):
3604+
raise ParseError(f"Layer '{layer}' does not exist!")
3605+
else:
3606+
rootDir = recipesRoot
35743607

35753608
config = self.loadConfigYaml(self.loadYaml, rootDir)
35763609
minVer = config.get("bobMinimumVersion", "0.16")
@@ -3593,7 +3626,7 @@ def __parseLayer(self, layerSpec, maxVer, recipesRoot):
35933626
# First parse any sub-layers. Their settings have a lower precedence
35943627
# and may be overwritten by higher layers.
35953628
for l in config.get("layers", []):
3596-
self.__parseLayer(l, maxVer, recipesRoot)
3629+
self.__parseLayer(l, maxVer, recipesRoot, layer)
35973630

35983631
# Load plugins and re-create schemas as new keys may have been added
35993632
self.__loadPlugins(rootDir, layer, config.get("plugins", []))

pym/bob/layers.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import schema
44
import shutil
55
from textwrap import indent
6-
from .errors import BuildError
6+
from .errors import BuildError, ParseError
77
from .invoker import CmdFailedError, InvocationError, Invoker
88
from .scm import getScm, ScmOverride, ScmStatus, ScmTaint
99
from .state import BobState
@@ -205,6 +205,9 @@ def getSubLayers(self):
205205
def getScm(self):
206206
return self.__scm
207207

208+
def getPolicy(self, name, location=None):
209+
return self.__config.getPolicy(name, location)
210+
208211
class Layers:
209212
def __init__(self, defines, attic):
210213
self.__layers = {}
@@ -260,7 +263,7 @@ def cleanupUnused(self):
260263
os.rename(d, atticPath)
261264
BobState().delLayerState(d)
262265

263-
def collect(self, loop, update, verbose=0):
266+
def collect(self, loop, update, verbose=0, requireManagedLayers=True):
264267
configSchema = (schema.Schema(RecipeSet.STATIC_CONFIG_LAYER_SPEC), b'')
265268
config = LayersConfig()
266269
with YamlCache() as yamlCache:
@@ -273,9 +276,16 @@ def collect(self, loop, update, verbose=0):
273276

274277
rootLayers = Layer("", config, self.__defines, self.__projectRoot)
275278
rootLayers.parse(yamlCache)
279+
if not rootLayers.getPolicy("managedLayers"):
280+
if requireManagedLayers:
281+
raise ParseError("Managed layers aren't enabled! See the managedLayers policy for details.")
282+
else:
283+
return False
276284
self.__layers[0] = rootLayers.getSubLayers();
277285
self.__collect(loop, 0, yamlCache, update, verbose)
278286

287+
return True
288+
279289
def setLayerConfig(self, configFiles):
280290
self.__layerConfigFiles = configFiles
281291

@@ -326,8 +336,8 @@ def status(self, printer):
326336
for (layerDir, status) in sorted(result.items()):
327337
printer(status, layerDir)
328338

329-
def updateLayers(loop, defines, verbose, attic, layerConfigs):
339+
def updateLayers(loop, defines, verbose, attic, layerConfigs, requireManagedLayers=True):
330340
layers = Layers(defines, attic)
331341
layers.setLayerConfig(layerConfigs)
332-
layers.collect(loop, True, verbose)
333-
layers.cleanupUnused()
342+
if layers.collect(loop, True, verbose, requireManagedLayers):
343+
layers.cleanupUnused()

test/black-box/layers-checkout/config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
bobMinimumVersion: "0.25.0rc2.dev6"
12
layers:
23
- name: foo
34
scm: git

test/black-box/layers-checkout/run.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,12 @@ run_bob layers update -DBAR_1_COMMIT=${bar_c1} -DBAR_2_COMMIT=${bar_c1} -DBAR_DI
188188
-lc layers_overrides -vv
189189
expect_exist layers/foo/override
190190

191+
# test that layers status/update are rejected on the old managedLayers policy
192+
old_dir="$tmp_dir/legacy"
193+
mkdir -p "$old_dir/recipes"
194+
expect_fail run_bob -C "$old_dir" layers update
195+
expect_fail run_bob -C "$old_dir" layers status
196+
191197
# remove layers + clean
192198
cleanup
193199
rm -rf layers

test/unit/test_input_recipeset.py

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,25 +34,24 @@ def tearDown(self):
3434
os.chdir(self.cwd)
3535
self.tmpdir.cleanup()
3636

37-
def writeRecipe(self, name, content, layer=None):
37+
def writeRecipe(self, name, content, layer=[]):
3838
path = os.path.join("",
39-
os.path.join("layers", layer) if layer is not None else "",
39+
*(os.path.join("layers", l) for l in layer),
4040
"recipes")
4141
if path: os.makedirs(path, exist_ok=True)
4242
with open(os.path.join(path, name+".yaml"), "w") as f:
4343
f.write(textwrap.dedent(content))
4444

45-
def writeClass(self, name, content, layer=None):
45+
def writeClass(self, name, content, layer=[]):
4646
path = os.path.join("",
47-
os.path.join("layers", layer) if layer is not None else "",
47+
*(os.path.join("layers", l) for l in layer),
4848
"classes")
4949
if path: os.makedirs(path, exist_ok=True)
5050
with open(os.path.join(path, name+".yaml"), "w") as f:
5151
f.write(textwrap.dedent(content))
5252

53-
def writeConfig(self, content, layer=None):
54-
path = os.path.join("",
55-
os.path.join("layers", layer) if layer is not None else "")
53+
def writeConfig(self, content, layer=[]):
54+
path = os.path.join("", *(os.path.join("layers", l) for l in layer))
5655
if path: os.makedirs(path, exist_ok=True)
5756
with open(os.path.join(path, "config.yaml"), "w") as f:
5857
f.write(yaml.dump(content))
@@ -1343,26 +1342,26 @@ def setUp(self):
13431342
self.writeConfig({
13441343
"bobMinimumVersion" : "0.24",
13451344
"layers" : [ "l2" ],
1346-
}, layer="l1_n1")
1345+
}, layer=["l1_n1"])
13471346
self.writeRecipe("foo", """\
13481347
depends:
13491348
- baz
13501349
buildScript: "true"
13511350
packageScript: "true"
13521351
""",
1353-
layer="l1_n1")
1352+
layer=["l1_n1"])
13541353

13551354
self.writeRecipe("baz", """\
13561355
buildScript: "true"
13571356
packageScript: "true"
13581357
""",
1359-
layer="l2")
1358+
layer=["l1_n1", "l2"])
13601359

13611360
self.writeRecipe("bar", """\
13621361
buildScript: "true"
13631362
packageScript: "true"
13641363
""",
1365-
layer="l1_n2")
1364+
layer=["l1_n2"])
13661365

13671366
def testRegular(self):
13681367
"""Test that layers can be parsed"""
@@ -1376,13 +1375,13 @@ def testRecipeObstruction(self):
13761375
buildScript: "true"
13771376
packageScript: "true"
13781377
""",
1379-
layer="l1_n2")
1378+
layer=["l1_n2"])
13801379
self.assertRaises(ParseError, self.generate)
13811380

13821381
def testClassObstruction(self):
13831382
"""Test that layers must not provide identical classes"""
1384-
self.writeClass("c", "", layer="l2")
1385-
self.writeClass("c", "", layer="l1_n2")
1383+
self.writeClass("c", "", layer=["l1_n1", "l2"])
1384+
self.writeClass("c", "", layer=["l1_n2"])
13861385
self.assertRaises(ParseError, self.generate)
13871386

13881387
def testMinimumVersion(self):
@@ -1393,6 +1392,22 @@ def testMinimumVersion(self):
13931392
})
13941393
self.assertRaises(ParseError, self.generate)
13951394

1395+
def testManagedRejected(self):
1396+
self.writeConfig({
1397+
"bobMinimumVersion" : "0.25rc1",
1398+
"layers" : [
1399+
{
1400+
"name" : "l1_n1",
1401+
"scm" : "git",
1402+
"url" : "[email protected]:bob.git",
1403+
}
1404+
]
1405+
})
1406+
with self.assertRaises(ParseError) as err:
1407+
self.generate()
1408+
self.assertEqual(err.exception.slogan,
1409+
"Managed layers aren't enabled! See the managedLayers policy for details.")
1410+
13961411
class TestIfExpression(RecipesTmp, TestCase):
13971412
""" Test if expressions """
13981413
def setUp(self):

0 commit comments

Comments
 (0)