Skip to content

Commit

Permalink
wrap: Clarify PackageDefinition API using dataclass
Browse files Browse the repository at this point in the history
This will simplify creating PackageDefinition objects from Cargo.lock
file. It contains basically the same information.
  • Loading branch information
xclaesse committed Mar 6, 2024
1 parent 5e0a307 commit d093f3c
Show file tree
Hide file tree
Showing 2 changed files with 117 additions and 108 deletions.
18 changes: 7 additions & 11 deletions mesonbuild/msubprojects.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,9 @@ def update_wrapdb(self) -> bool:
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)
Expand Down Expand Up @@ -520,17 +522,11 @@ def purge(self) -> bool:
if not self.wrap.type:
return True

if self.wrap.redirected:
redirect_file = Path(self.wrap.original_filename).resolve()
if self.wrap.original_filename:
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()
Expand Down
207 changes: 110 additions & 97 deletions mesonbuild/wrap/wrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from .. import mlog
import contextlib
from dataclasses import dataclass
import dataclasses
import urllib.request
import urllib.error
import urllib.parse
Expand Down Expand Up @@ -136,47 +136,64 @@ class WrapException(MesonException):
class WrapNotFoundException(WrapException):
pass

@dataclasses.dataclass(eq=False)
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] = {}
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()
name: str
subprojects_dir: str
type: T.Optional[str] = None
values: T.Dict[str, str] = dataclasses.field(default_factory=dict)
provided_deps: T.Dict[str, T.Optional[str]] = dataclasses.field(default_factory=dict)
provided_programs: T.List[str] = dataclasses.field(default_factory=list)
diff_files: T.List[Path] = dataclasses.field(default_factory=list)
wrapfile_hash: T.Optional[str] = None
original_filename: T.Optional[str] = None
filedir: str = dataclasses.field(init=False)
directory: str = dataclasses.field(init=False)

def __post_init__(self) -> None:
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 == '..':
Expand All @@ -186,37 +203,40 @@ def parse_wrap(self) -> None:
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
return wrap

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

def parse_wrap_section(self, config: configparser.ConfigParser) -> None:
@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'):
Expand All @@ -236,7 +256,7 @@ def parse_provide_section(self, config: configparser.ConfigParser) -> None:
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)
Expand All @@ -246,20 +266,21 @@ def get(self, key: str) -> str:
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

Expand All @@ -272,11 +293,11 @@ def verbose_git(cmd: T.List[str], workingdir: str, check: bool = False) -> bool:
except mesonlib.GitException as e:
raise WrapException(str(e))

@dataclass(eq=False)
@dataclasses.dataclass(eq=False)
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
Expand Down Expand Up @@ -313,15 +334,15 @@ def load_wraps(self) -> None:
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.
for i in dirs:
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():
Expand All @@ -331,13 +352,13 @@ def add_wrap(self, wrap: PackageDefinition) -> None:
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

Expand All @@ -363,7 +384,7 @@ def get_from_wrapdb(self, subp_name: str) -> T.Optional[PackageDefinition]:
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
Expand Down Expand Up @@ -409,32 +430,26 @@ def resolve(self, packagename: str, force_method: T.Optional[Method] = None) ->
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 not os.path.samefile(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] = {
Expand Down Expand Up @@ -497,7 +512,7 @@ def has_buildfile() -> bool:
raise

if not has_buildfile():
raise WrapException(f'Subproject exists but has no {methods_map[method]} file.')
raise WrapException(f'Subproject {packagename} exists but has no {methods_map[method]} file.')

# At this point, the subproject has been successfully resolved for the
# first time so save off the hash of the entire wrap file for future
Expand Down Expand Up @@ -606,7 +621,7 @@ def _get_git(self, packagename: str) -> None:

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.
Expand All @@ -618,10 +633,8 @@ def validate(self) -> None:
# 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:
Expand Down Expand Up @@ -783,7 +796,7 @@ def _get_file_internal(self, what: str, packagename: str) -> str:

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)
Expand Down

0 comments on commit d093f3c

Please sign in to comment.