wrap: Clarify PackageDefinition API

This will simplify creating PackageDefinition objects from Cargo.lock
file. It contains basically the same information.
pull/12945/head
Xavier Claessens 9 months ago committed by Xavier Claessens
parent ce889d6870
commit c0de2e1264
  1. 16
      mesonbuild/msubprojects.py
  2. 195
      mesonbuild/wrap/wrap.py
  3. 8
      unittests/allplatformstests.py

@ -176,7 +176,9 @@ class Runner:
latest_version = info['versions'][0]
new_branch, new_revision = latest_version.rsplit('-', 1)
if new_branch != branch or new_revision != revision:
filename = self.wrap.filename if self.wrap.has_wrap else f'{self.wrap.filename}.wrap'
filename = self.wrap.original_filename
if not filename:
filename = os.path.join(self.wrap.subprojects_dir, f'{self.wrap.name}.wrap')
update_wrap_file(filename, self.wrap.name,
new_branch, new_revision,
options.allow_insecure)
@ -521,16 +523,10 @@ class Runner:
return True
if self.wrap.redirected:
redirect_file = Path(self.wrap.original_filename).resolve()
wrapfile = Path(self.wrap.original_filename).resolve()
if options.confirm:
redirect_file.unlink()
mlog.log(f'Deleting {redirect_file}')
if self.wrap.type == 'redirect':
redirect_file = Path(self.wrap.filename).resolve()
if options.confirm:
redirect_file.unlink()
self.log(f'Deleting {redirect_file}')
wrapfile.unlink()
mlog.log(f'Deleting {wrapfile}')
if options.include_cache:
packagecache = Path(self.wrap_resolver.cachedir).resolve()

@ -51,7 +51,7 @@ except ImportError:
REQ_TIMEOUT = 30.0
WHITELIST_SUBDOMAIN = 'wrapdb.mesonbuild.com'
ALL_TYPES = ['file', 'git', 'hg', 'svn']
ALL_TYPES = ['file', 'git', 'hg', 'svn', 'redirect']
PATCH = shutil.which('patch')
@ -137,46 +137,60 @@ class WrapNotFoundException(WrapException):
pass
class PackageDefinition:
def __init__(self, fname: str, subproject: str = ''):
self.filename = fname
self.subproject = SubProject(subproject)
self.type: T.Optional[str] = None
self.values: T.Dict[str, str] = {}
def __init__(self, name: str, subprojects_dir: str, type_: T.Optional[str] = None, values: T.Optional[T.Dict[str, str]] = None):
self.name = name
self.subprojects_dir = subprojects_dir
self.type = type_
self.values = values or {}
self.provided_deps: T.Dict[str, T.Optional[str]] = {}
self.provided_programs: T.List[str] = []
self.diff_files: T.List[Path] = []
self.basename = os.path.basename(fname)
self.has_wrap = self.basename.endswith('.wrap')
self.name = self.basename[:-5] if self.has_wrap else self.basename
# must be lowercase for consistency with dep=variable assignment
self.provided_deps[self.name.lower()] = None
# What the original file name was before redirection
self.original_filename = fname
self.redirected = False
if self.has_wrap:
self.parse_wrap()
with open(fname, 'r', encoding='utf-8') as file:
self.wrapfile_hash = hashlib.sha256(file.read().encode('utf-8')).hexdigest()
self.wrapfile_hash: T.Optional[str] = None
self.original_filename: T.Optional[str] = None
self.redirected: bool = False
self.filesdir = os.path.join(self.subprojects_dir, 'packagefiles')
self.directory = self.values.get('directory', self.name)
if os.path.dirname(self.directory):
raise WrapException('Directory key must be a name and not a path')
if self.type and self.type not in ALL_TYPES:
raise WrapException(f'Unknown wrap type {self.type!r}')
self.filesdir = os.path.join(os.path.dirname(self.filename), 'packagefiles')
if 'diff_files' in self.values:
for s in self.values['diff_files'].split(','):
path = Path(s.strip())
if path.is_absolute():
raise WrapException('diff_files paths cannot be absolute')
if '..' in path.parts:
raise WrapException('diff_files paths cannot contain ".."')
self.diff_files.append(path)
# must be lowercase for consistency with dep=variable assignment
self.provided_deps[self.name.lower()] = None
def parse_wrap(self) -> None:
try:
config = configparser.ConfigParser(interpolation=None)
config.read(self.filename, encoding='utf-8')
except configparser.Error as e:
raise WrapException(f'Failed to parse {self.basename}: {e!s}')
self.parse_wrap_section(config)
if self.type == 'redirect':
@staticmethod
def from_values(name: str, subprojects_dir: str, type_: str, values: T.Dict[str, str]) -> PackageDefinition:
return PackageDefinition(name, subprojects_dir, type_, values)
@staticmethod
def from_directory(filename: str) -> PackageDefinition:
name = os.path.basename(filename)
subprojects_dir = os.path.dirname(filename)
return PackageDefinition(name, subprojects_dir)
@staticmethod
def from_wrap_file(filename: str, subproject: SubProject = SubProject('')) -> PackageDefinition:
config, type_, values = PackageDefinition._parse_wrap(filename)
if 'diff_files' in values:
FeatureNew('Wrap files with diff_files', '0.63.0').use(subproject)
if 'patch_directory' in values:
FeatureNew('Wrap files with patch_directory', '0.55.0').use(subproject)
for what in ['patch', 'source']:
if f'{what}_filename' in values and f'{what}_url' not in values:
FeatureNew(f'Local wrap patch files without {what}_url', '0.55.0').use(subproject)
subprojects_dir = os.path.dirname(filename)
if type_ == 'redirect':
# [wrap-redirect] have a `filename` value pointing to the real wrap
# file we should parse instead. It must be relative to the current
# wrap file location and must be in the form foo/subprojects/bar.wrap.
dirname = Path(self.filename).parent
fname = Path(self.values['filename'])
fname = Path(values['filename'])
for i, p in enumerate(fname.parts):
if i % 2 == 0:
if p == '..':
@ -186,37 +200,41 @@ class PackageDefinition:
raise WrapException('wrap-redirect filename must be in the form foo/subprojects/bar.wrap')
if fname.suffix != '.wrap':
raise WrapException('wrap-redirect filename must be a .wrap file')
fname = dirname / fname
fname = Path(subprojects_dir, fname)
if not fname.is_file():
raise WrapException(f'wrap-redirect {fname} filename does not exist')
self.filename = str(fname)
self.parse_wrap()
self.redirected = True
else:
self.parse_provide_section(config)
if 'patch_directory' in self.values:
FeatureNew('Wrap files with patch_directory', '0.55.0').use(self.subproject)
for what in ['patch', 'source']:
if f'{what}_filename' in self.values and f'{what}_url' not in self.values:
FeatureNew(f'Local wrap patch files without {what}_url', '0.55.0').use(self.subproject)
wrap = PackageDefinition.from_wrap_file(str(fname), subproject)
wrap.original_filename = filename
wrap.redirected = True
return wrap
def parse_wrap_section(self, config: configparser.ConfigParser) -> None:
name = os.path.basename(filename)[:-5]
wrap = PackageDefinition.from_values(name, subprojects_dir, type_, values)
wrap.original_filename = filename
wrap.parse_provide_section(config)
with open(filename, 'r', encoding='utf-8') as file:
wrap.wrapfile_hash = hashlib.sha256(file.read().encode('utf-8')).hexdigest()
return wrap
@staticmethod
def _parse_wrap(filename: str) -> T.Tuple[configparser.ConfigParser, str, T.Dict[str, str]]:
try:
config = configparser.ConfigParser(interpolation=None)
config.read(filename, encoding='utf-8')
except configparser.Error as e:
raise WrapException(f'Failed to parse {filename}: {e!s}')
if len(config.sections()) < 1:
raise WrapException(f'Missing sections in {self.basename}')
self.wrap_section = config.sections()[0]
if not self.wrap_section.startswith('wrap-'):
raise WrapException(f'{self.wrap_section!r} is not a valid first section in {self.basename}')
self.type = self.wrap_section[5:]
self.values = dict(config[self.wrap_section])
if 'diff_files' in self.values:
FeatureNew('Wrap files with diff_files', '0.63.0').use(self.subproject)
for s in self.values['diff_files'].split(','):
path = Path(s.strip())
if path.is_absolute():
raise WrapException('diff_files paths cannot be absolute')
if '..' in path.parts:
raise WrapException('diff_files paths cannot contain ".."')
self.diff_files.append(path)
raise WrapException(f'Missing sections in {filename}')
wrap_section = config.sections()[0]
if not wrap_section.startswith('wrap-'):
raise WrapException(f'{wrap_section!r} is not a valid first section in {filename}')
type_ = wrap_section[5:]
if type_ not in ALL_TYPES:
raise WrapException(f'Unknown wrap type {type_!r}')
values = dict(config[wrap_section])
return config, type_, values
def parse_provide_section(self, config: configparser.ConfigParser) -> None:
if config.has_section('provides'):
@ -236,7 +254,7 @@ class PackageDefinition:
self.provided_programs += names_list
continue
if not v:
m = (f'Empty dependency variable name for {k!r} in {self.basename}. '
m = (f'Empty dependency variable name for {k!r} in {self.name}.wrap. '
'If the subproject uses meson.override_dependency() '
'it can be added in the "dependency_names" special key.')
raise WrapException(m)
@ -246,20 +264,21 @@ class PackageDefinition:
try:
return self.values[key]
except KeyError:
raise WrapException(f'Missing key {key!r} in {self.basename}')
raise WrapException(f'Missing key {key!r} in {self.name}.wrap')
def get_hashfile(self, subproject_directory: str) -> str:
@staticmethod
def get_hashfile(subproject_directory: str) -> str:
return os.path.join(subproject_directory, '.meson-subproject-wrap-hash.txt')
def update_hash_cache(self, subproject_directory: str) -> None:
if self.has_wrap:
if self.wrapfile_hash:
with open(self.get_hashfile(subproject_directory), 'w', encoding='utf-8') as file:
file.write(self.wrapfile_hash + '\n')
def get_directory(subdir_root: str, packagename: str) -> str:
fname = os.path.join(subdir_root, packagename + '.wrap')
if os.path.isfile(fname):
wrap = PackageDefinition(fname)
wrap = PackageDefinition.from_wrap_file(fname)
return wrap.directory
return packagename
@ -276,7 +295,7 @@ def verbose_git(cmd: T.List[str], workingdir: str, check: bool = False) -> bool:
class Resolver:
source_dir: str
subdir: str
subproject: str = ''
subproject: SubProject = SubProject('')
wrap_mode: WrapMode = WrapMode.default
wrap_frontend: bool = False
allow_insecure: bool = False
@ -313,7 +332,7 @@ class Resolver:
if not i.endswith('.wrap'):
continue
fname = os.path.join(self.subdir_root, i)
wrap = PackageDefinition(fname, self.subproject)
wrap = PackageDefinition.from_wrap_file(fname, self.subproject)
self.wraps[wrap.name] = wrap
ignore_dirs |= {wrap.directory, wrap.name}
# Add dummy package definition for directories not associated with a wrap file.
@ -321,7 +340,7 @@ class Resolver:
if i in ignore_dirs:
continue
fname = os.path.join(self.subdir_root, i)
wrap = PackageDefinition(fname, self.subproject)
wrap = PackageDefinition.from_directory(fname)
self.wraps[wrap.name] = wrap
for wrap in self.wraps.values():
@ -331,13 +350,13 @@ class Resolver:
for k in wrap.provided_deps.keys():
if k in self.provided_deps:
prev_wrap = self.provided_deps[k]
m = f'Multiple wrap files provide {k!r} dependency: {wrap.basename} and {prev_wrap.basename}'
m = f'Multiple wrap files provide {k!r} dependency: {wrap.name} and {prev_wrap.name}'
raise WrapException(m)
self.provided_deps[k] = wrap
for k in wrap.provided_programs:
if k in self.provided_programs:
prev_wrap = self.provided_programs[k]
m = f'Multiple wrap files provide {k!r} program: {wrap.basename} and {prev_wrap.basename}'
m = f'Multiple wrap files provide {k!r} program: {wrap.name} and {prev_wrap.name}'
raise WrapException(m)
self.provided_programs[k] = wrap
@ -363,7 +382,7 @@ class Resolver:
with fname.open('wb') as f:
f.write(url.read())
mlog.log(f'Installed {subp_name} version {version} revision {revision}')
wrap = PackageDefinition(str(fname))
wrap = PackageDefinition.from_wrap_file(str(fname))
self.wraps[wrap.name] = wrap
self.add_wrap(wrap)
return wrap
@ -409,32 +428,26 @@ class Resolver:
raise WrapNotFoundException(f'Neither a subproject directory nor a {packagename}.wrap file was found.')
self.wrap = wrap
self.directory = self.wrap.directory
self.dirname = os.path.join(self.wrap.subprojects_dir, self.wrap.directory)
if not os.path.exists(self.dirname):
self.dirname = os.path.join(self.subdir_root, self.directory)
rel_path = os.path.relpath(self.dirname, self.source_dir)
if self.wrap.has_wrap:
# We have a .wrap file, use directory relative to the location of
# the wrap file if it exists, otherwise source code will be placed
# into main project's subproject_dir even if the wrap file comes
# from another subproject.
self.dirname = os.path.join(os.path.dirname(self.wrap.filename), self.wrap.directory)
if not os.path.exists(self.dirname):
self.dirname = os.path.join(self.subdir_root, self.directory)
# Check if the wrap comes from the main project.
main_fname = os.path.join(self.subdir_root, self.wrap.basename)
if self.wrap.filename != main_fname:
rel = os.path.relpath(self.wrap.filename, self.source_dir)
if self.wrap.original_filename:
# If the original wrap file is not in main project's subproject_dir,
# write a wrap-redirect.
basename = os.path.basename(self.wrap.original_filename)
main_fname = os.path.join(self.subdir_root, basename)
if self.wrap.original_filename != main_fname:
rel = os.path.relpath(self.wrap.original_filename, self.source_dir)
mlog.log('Using', mlog.bold(rel))
# Write a dummy wrap file in main project that redirect to the
# wrap we picked.
with open(main_fname, 'w', encoding='utf-8') as f:
f.write(textwrap.dedent(f'''\
[wrap-redirect]
filename = {PurePath(os.path.relpath(self.wrap.filename, self.subdir_root)).as_posix()}
filename = {PurePath(os.path.relpath(self.wrap.original_filename, self.subdir_root)).as_posix()}
'''))
else:
# No wrap file, it's a dummy package definition for an existing
# directory. Use the source code in place.
self.dirname = self.wrap.filename
rel_path = os.path.relpath(self.dirname, self.source_dir)
# Map each supported method to a file that must exist at the root of source tree.
methods_map: T.Dict[Method, str] = {
@ -606,7 +619,7 @@ class Resolver:
def validate(self) -> None:
# This check is only for subprojects with wraps.
if not self.wrap.has_wrap:
if not self.wrap.wrapfile_hash:
return
# Retrieve original hash, if it exists.
@ -618,10 +631,8 @@ class Resolver:
# If stored hash doesn't exist then don't warn.
return
actual_hash = self.wrap.wrapfile_hash
# Compare hashes and warn the user if they don't match.
if expected_hash != actual_hash:
if expected_hash != self.wrap.wrapfile_hash:
mlog.warning(f'Subproject {self.wrap.name}\'s revision may be out of date; its wrap file has changed since it was first configured')
def is_git_full_commit_id(self, revno: str) -> bool:
@ -783,7 +794,7 @@ class Resolver:
def apply_patch(self, packagename: str) -> None:
if 'patch_filename' in self.wrap.values and 'patch_directory' in self.wrap.values:
m = f'Wrap file {self.wrap.basename!r} must not have both "patch_filename" and "patch_directory"'
m = f'Wrap file {self.wrap.name!r} must not have both "patch_filename" and "patch_directory"'
raise WrapException(m)
if 'patch_filename' in self.wrap.values:
path = self._get_file_internal('patch', packagename)

@ -4217,7 +4217,7 @@ class AllPlatformTests(BasePlatformTests):
filename = foo/subprojects/real.wrapper
'''))
with self.assertRaisesRegex(WrapException, 'wrap-redirect filename must be a .wrap file'):
PackageDefinition(redirect_wrap)
PackageDefinition.from_wrap_file(redirect_wrap)
# Invalid redirect, filename cannot be in parent directory
with open(redirect_wrap, 'w', encoding='utf-8') as f:
@ -4226,7 +4226,7 @@ class AllPlatformTests(BasePlatformTests):
filename = ../real.wrap
'''))
with self.assertRaisesRegex(WrapException, 'wrap-redirect filename cannot contain ".."'):
PackageDefinition(redirect_wrap)
PackageDefinition.from_wrap_file(redirect_wrap)
# Invalid redirect, filename must be in foo/subprojects/real.wrap
with open(redirect_wrap, 'w', encoding='utf-8') as f:
@ -4235,7 +4235,7 @@ class AllPlatformTests(BasePlatformTests):
filename = foo/real.wrap
'''))
with self.assertRaisesRegex(WrapException, 'wrap-redirect filename must be in the form foo/subprojects/bar.wrap'):
PackageDefinition(redirect_wrap)
PackageDefinition.from_wrap_file(redirect_wrap)
# Correct redirect
with open(redirect_wrap, 'w', encoding='utf-8') as f:
@ -4248,7 +4248,7 @@ class AllPlatformTests(BasePlatformTests):
[wrap-git]
url = http://invalid
'''))
wrap = PackageDefinition(redirect_wrap)
wrap = PackageDefinition.from_wrap_file(redirect_wrap)
self.assertEqual(wrap.get('url'), 'http://invalid')
@skip_if_no_cmake

Loading…
Cancel
Save