manifest: support minimum required west version

As we evolve the manifest data, we want to be able to enforce a
minimum version of west required to parse it. Add an optional
'version' key to the data to make this possible.

We have to validate this before checking the manifest against the
schema, as later versions of west will likely extend the schema in
ways that would raise errors if we used our schema to check the data.

We allow the version to be parsed as a YAML float (like 1.0) and
convert it to a string as a convenience for the user.

Signed-off-by: Marti Bolivar <marti.bolivar@nordicsemi.no>
This commit is contained in:
Marti Bolivar 2019-10-08 09:39:30 -07:00 committed by Carles Cufí
parent 428ef11b38
commit 18857356bd
4 changed files with 107 additions and 2 deletions

View File

@ -42,6 +42,7 @@ setuptools.setup(
'pykwalify',
'configobj',
'setuptools>=v40.1.0', # for find_namespace_packages
'packaging',
],
python_requires='>=3.4',
entry_points={'console_scripts': ('west = west.main:main',)},

View File

@ -17,6 +17,20 @@
type: map
mapping:
# The "version" key is optional, and specifies a minimum version of
# west (in semantic versioning, so X.Y <= X.Y.0 < X.Y.1 < (X+1).Y,
# etc.) that is required to correctly parse the manifest data.
#
# This is supported starting with west v0.7 (i.e. it was added after
# 0.6 was released). Earlier versions of west will treat manifest
# data with a version key as malformed, so it can reliably be used
# to prevent parsing on west v0.5 and v0.6 as well, though the error
# messages users will receive will be a bit more confusing in that
# case.
version:
required: false
type: text
# The "defaults" key specifies some default values used in the
# rest of the manifest.
defaults:

View File

@ -26,12 +26,14 @@ import shutil
import shlex
import subprocess
from packaging.version import parse as parse_version
import pykwalify.core
import yaml
from west import util, log
from west.backports import CompletedProcess
import west.configuration as cfg
from west.version import __version__ as west_version
#: Index in projects where the project with contains project manifest file is
@ -231,6 +233,24 @@ class Manifest:
self._malformed('manifest contains no manifest element')
data = self._data['manifest']
# Make sure this version of west can load this manifest data.
# This has to happen before the schema check -- later schemas
# are likely to incompatibly extend our own.
if 'version' in data:
min_version = data['version']
# As a convenience for the user, convert floats to strings.
# This avoids forcing them to write:
#
# version: "1.0"
#
# by explicitly allowing:
#
# version: 1.0
if isinstance(min_version, float):
min_version = str(min_version)
if parse_version(min_version) > _WEST_VERSION:
raise ManifestVersionError(min_version, file=source_file)
try:
pykwalify.core.Core(source_data=data,
schema_files=[_SCHEMA_PATH]).validate()
@ -511,6 +531,17 @@ class MalformedConfig(Exception):
'''Exception indicating that west config is malformed and thus causing west
manifest parsing to fail.'''
class ManifestVersionError(Exception):
'''The manifest required a version of west more recent than the
current running version.
'''
def __init__(self, version, file=None):
self.version = version
'''The minimum version of west that was required.'''
self.file = file
'''The file that required this version of west.'''
# Definitions for Manifest attribute types.
@ -949,7 +980,7 @@ class ManifestProject(Project):
_SCHEMA_PATH = os.path.join(os.path.dirname(__file__), "manifest-schema.yml")
_DEFAULTS = Defaults()
_WEST_VERSION = parse_version(west_version)
@lru_cache(maxsize=1)
def _warn_once_if_no_git():

View File

@ -11,7 +11,8 @@ import pytest
import yaml
from west.manifest import Manifest, Defaults, Remote, Project, \
ManifestProject, MalformedManifest, manifest_path
ManifestProject, MalformedManifest, ManifestVersionError, \
manifest_path
THIS_DIRECTORY = os.path.dirname(__file__)
@ -768,6 +769,64 @@ def test_load_str():
''')
assert manifest.projects[-1].name == 'foo'
def test_version_check_failure():
# Check that the manifest.version key causes manifest parsing to
# fail when it should.
valid_fmt = '''\
manifest:
version: {}
projects:
- name: foo
url: https://foo.com
'''
invalid_fmt = '''\
manifest:
version: {}
projects:
- name: foo
url: https://foo.com
pytest-invalid-key: a-value
'''
# Parsing a well-formed manifest for a version of west greater
# than our own should raise ManifestVersionError.
#
# This should be the case whether the version is a string (as is
# usual) or, as a special case to work around YAML syntax rules, a
# float.
with pytest.raises(ManifestVersionError):
Manifest.from_data(valid_fmt.format('"99.0"'))
with pytest.raises(ManifestVersionError):
Manifest.from_data(valid_fmt.format('99.0'))
# Parsing Manifests with unsatisfiable version requirements should
# *not* raise MalformedManifest, even if they have unrecognized keys.
with pytest.raises(ManifestVersionError):
Manifest.from_data(invalid_fmt.format('"99.0"'))
with pytest.raises(ManifestVersionError):
Manifest.from_data(invalid_fmt.format('99.0'))
@pytest.mark.parametrize(
'ver', ['0.6', '0.6.2', '0.6.0.dev1', '0.6.0rc1', '0.6.99'])
def test_version_check_success(ver):
# Test that version checking succeeds when it should.
#
# Parsing a well-formed manifest for a version of west no greater
# than this one, including RC and dev versions used for
# pre-releases, should not raise this error.
fmt = '''\
manifest:
version: {}
projects:
- name: foo
url: https://foo.com
'''
manifest = Manifest.from_data(fmt.format(ver))
assert manifest.projects[-1].name == 'foo'
manifest = Manifest.from_data(fmt.format('"' + ver + '"'))
assert manifest.projects[-1].name == 'foo'
# Invalid manifests should raise MalformedManifest.
@pytest.mark.parametrize('invalid',