summaryrefslogtreecommitdiffstats
path: root/mesonbuild/wrap
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--mesonbuild/wrap/__init__.py59
-rw-r--r--mesonbuild/wrap/wrap.py833
-rw-r--r--mesonbuild/wrap/wraptool.py231
3 files changed, 1123 insertions, 0 deletions
diff --git a/mesonbuild/wrap/__init__.py b/mesonbuild/wrap/__init__.py
new file mode 100644
index 0000000..653f42a
--- /dev/null
+++ b/mesonbuild/wrap/__init__.py
@@ -0,0 +1,59 @@
+from enum import Enum
+
+# Used for the --wrap-mode command-line argument
+#
+# Special wrap modes:
+# nofallback: Don't download wraps for dependency() fallbacks
+# nodownload: Don't download wraps for all subproject() calls
+#
+# subprojects are used for two purposes:
+# 1. To download and build dependencies by using .wrap
+# files if they are not provided by the system. This is
+# usually expressed via dependency(..., fallback: ...).
+# 2. To download and build 'copylibs' which are meant to be
+# used by copying into your project. This is always done
+# with an explicit subproject() call.
+#
+# --wrap-mode=nofallback will never do (1)
+# --wrap-mode=nodownload will do neither (1) nor (2)
+#
+# If you are building from a release tarball, you should be
+# able to safely use 'nodownload' since upstream is
+# expected to ship all required sources with the tarball.
+#
+# If you are building from a git repository, you will want
+# to use 'nofallback' so that any 'copylib' wraps will be
+# download as subprojects.
+#
+# --wrap-mode=forcefallback will ignore external dependencies,
+# even if they match the version requirements, and automatically
+# use the fallback if one was provided. This is useful for example
+# to make sure a project builds when using the fallbacks.
+#
+# Note that these options do not affect subprojects that
+# are git submodules since those are only usable in git
+# repositories, and you almost always want to download them.
+
+# This did _not_ work when inside the WrapMode class.
+# I don't know why. If you can fix this, patches welcome.
+string_to_value = {'default': 1,
+ 'nofallback': 2,
+ 'nodownload': 3,
+ 'forcefallback': 4,
+ 'nopromote': 5,
+ }
+
+class WrapMode(Enum):
+ default = 1
+ nofallback = 2
+ nodownload = 3
+ forcefallback = 4
+ nopromote = 5
+
+ def __str__(self) -> str:
+ return self.name
+
+ @staticmethod
+ def from_string(mode_name: str) -> 'WrapMode':
+ g = string_to_value[mode_name]
+ return WrapMode(g)
diff --git a/mesonbuild/wrap/wrap.py b/mesonbuild/wrap/wrap.py
new file mode 100644
index 0000000..9b1795b
--- /dev/null
+++ b/mesonbuild/wrap/wrap.py
@@ -0,0 +1,833 @@
+# Copyright 2015 The Meson development team
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from __future__ import annotations
+
+from .. import mlog
+import contextlib
+from dataclasses import dataclass
+import urllib.request
+import urllib.error
+import urllib.parse
+import os
+import hashlib
+import shutil
+import tempfile
+import stat
+import subprocess
+import sys
+import configparser
+import time
+import typing as T
+import textwrap
+import json
+
+from base64 import b64encode
+from netrc import netrc
+from pathlib import Path
+
+from . import WrapMode
+from .. import coredata
+from ..mesonlib import quiet_git, GIT, ProgressBar, MesonException, windows_proof_rmtree, Popen_safe
+from ..interpreterbase import FeatureNew
+from ..interpreterbase import SubProject
+from .. import mesonlib
+
+if T.TYPE_CHECKING:
+ import http.client
+
+try:
+ # Importing is just done to check if SSL exists, so all warnings
+ # regarding 'imported but unused' can be safely ignored
+ import ssl # noqa
+ has_ssl = True
+except ImportError:
+ has_ssl = False
+
+REQ_TIMEOUT = 600.0
+SSL_WARNING_PRINTED = False
+WHITELIST_SUBDOMAIN = 'wrapdb.mesonbuild.com'
+
+ALL_TYPES = ['file', 'git', 'hg', 'svn']
+
+PATCH = shutil.which('patch')
+
+def whitelist_wrapdb(urlstr: str) -> urllib.parse.ParseResult:
+ """ raises WrapException if not whitelisted subdomain """
+ url = urllib.parse.urlparse(urlstr)
+ if not url.hostname:
+ raise WrapException(f'{urlstr} is not a valid URL')
+ if not url.hostname.endswith(WHITELIST_SUBDOMAIN):
+ raise WrapException(f'{urlstr} is not a whitelisted WrapDB URL')
+ if has_ssl and not url.scheme == 'https':
+ raise WrapException(f'WrapDB did not have expected SSL https url, instead got {urlstr}')
+ return url
+
+def open_wrapdburl(urlstring: str, allow_insecure: bool = False, have_opt: bool = False) -> 'http.client.HTTPResponse':
+ if have_opt:
+ insecure_msg = '\n\n To allow connecting anyway, pass `--allow-insecure`.'
+ else:
+ insecure_msg = ''
+
+ url = whitelist_wrapdb(urlstring)
+ if has_ssl:
+ try:
+ return T.cast('http.client.HTTPResponse', urllib.request.urlopen(urllib.parse.urlunparse(url), timeout=REQ_TIMEOUT))
+ except urllib.error.URLError as excp:
+ msg = f'WrapDB connection failed to {urlstring} with error {excp}.'
+ if isinstance(excp.reason, ssl.SSLCertVerificationError):
+ if allow_insecure:
+ mlog.warning(f'{msg}\n\n Proceeding without authentication.')
+ else:
+ raise WrapException(f'{msg}{insecure_msg}')
+ else:
+ raise WrapException(msg)
+ elif not allow_insecure:
+ raise WrapException(f'SSL module not available in {sys.executable}: Cannot contact the WrapDB.{insecure_msg}')
+ else:
+ # following code is only for those without Python SSL
+ global SSL_WARNING_PRINTED # pylint: disable=global-statement
+ if not SSL_WARNING_PRINTED:
+ mlog.warning(f'SSL module not available in {sys.executable}: WrapDB traffic not authenticated.')
+ SSL_WARNING_PRINTED = True
+
+ # If we got this far, allow_insecure was manually passed
+ nossl_url = url._replace(scheme='http')
+ try:
+ return T.cast('http.client.HTTPResponse', urllib.request.urlopen(urllib.parse.urlunparse(nossl_url), timeout=REQ_TIMEOUT))
+ except urllib.error.URLError as excp:
+ raise WrapException(f'WrapDB connection failed to {urlstring} with error {excp}')
+
+def get_releases_data(allow_insecure: bool) -> bytes:
+ url = open_wrapdburl('https://wrapdb.mesonbuild.com/v2/releases.json', allow_insecure, True)
+ return url.read()
+
+def get_releases(allow_insecure: bool) -> T.Dict[str, T.Any]:
+ data = get_releases_data(allow_insecure)
+ return T.cast('T.Dict[str, T.Any]', json.loads(data.decode()))
+
+def update_wrap_file(wrapfile: str, name: str, new_version: str, new_revision: str, allow_insecure: bool) -> None:
+ url = open_wrapdburl(f'https://wrapdb.mesonbuild.com/v2/{name}_{new_version}-{new_revision}/{name}.wrap',
+ allow_insecure, True)
+ with open(wrapfile, 'wb') as f:
+ f.write(url.read())
+
+def parse_patch_url(patch_url: str) -> T.Tuple[str, str]:
+ u = urllib.parse.urlparse(patch_url)
+ if u.netloc != 'wrapdb.mesonbuild.com':
+ raise WrapException(f'URL {patch_url} does not seems to be a wrapdb patch')
+ arr = u.path.strip('/').split('/')
+ if arr[0] == 'v1':
+ # e.g. https://wrapdb.mesonbuild.com/v1/projects/zlib/1.2.11/5/get_zip
+ return arr[-3], arr[-2]
+ elif arr[0] == 'v2':
+ # e.g. https://wrapdb.mesonbuild.com/v2/zlib_1.2.11-5/get_patch
+ tag = arr[-2]
+ _, version = tag.rsplit('_', 1)
+ version, revision = version.rsplit('-', 1)
+ return version, revision
+ else:
+ raise WrapException(f'Invalid wrapdb URL {patch_url}')
+
+class WrapException(MesonException):
+ pass
+
+class WrapNotFoundException(WrapException):
+ pass
+
+class PackageDefinition:
+ def __init__(self, fname: str, subproject: str = ''):
+ self.filename = fname
+ self.subproject = SubProject(subproject)
+ self.type = None # type: T.Optional[str]
+ self.values = {} # type: T.Dict[str, str]
+ self.provided_deps = {} # type: T.Dict[str, T.Optional[str]]
+ self.provided_programs = [] # type: T.List[str]
+ self.diff_files = [] # type: 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.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')
+
+ 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':
+ # [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'])
+ for i, p in enumerate(fname.parts):
+ if i % 2 == 0:
+ if p == '..':
+ raise WrapException('wrap-redirect filename cannot contain ".."')
+ else:
+ if p != 'subprojects':
+ 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
+ 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)
+
+ def parse_wrap_section(self, config: configparser.ConfigParser) -> None:
+ 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)
+
+ def parse_provide_section(self, config: configparser.ConfigParser) -> None:
+ if config.has_section('provide'):
+ for k, v in config['provide'].items():
+ if k == 'dependency_names':
+ # A comma separated list of dependency names that does not
+ # need a variable name; must be lowercase for consistency with
+ # dep=variable assignment
+ names_dict = {n.strip().lower(): None for n in v.split(',')}
+ self.provided_deps.update(names_dict)
+ continue
+ if k == 'program_names':
+ # A comma separated list of program names
+ names_list = [n.strip() for n in v.split(',')]
+ self.provided_programs += names_list
+ continue
+ if not v:
+ m = (f'Empty dependency variable name for {k!r} in {self.basename}. '
+ 'If the subproject uses meson.override_dependency() '
+ 'it can be added in the "dependency_names" special key.')
+ raise WrapException(m)
+ self.provided_deps[k] = v
+
+ def get(self, key: str) -> str:
+ try:
+ return self.values[key]
+ except KeyError:
+ raise WrapException(f'Missing key {key!r} in {self.basename}')
+
+ def get_hashfile(self, 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:
+ 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)
+ return wrap.directory
+ return packagename
+
+def verbose_git(cmd: T.List[str], workingdir: str, check: bool = False) -> bool:
+ '''
+ Wrapper to convert GitException to WrapException caught in interpreter.
+ '''
+ try:
+ return mesonlib.verbose_git(cmd, workingdir, check=check)
+ except mesonlib.GitException as e:
+ raise WrapException(str(e))
+
+@dataclass(eq=False)
+class Resolver:
+ source_dir: str
+ subdir: str
+ subproject: str = ''
+ wrap_mode: WrapMode = WrapMode.default
+ wrap_frontend: bool = False
+ allow_insecure: bool = False
+
+ def __post_init__(self) -> None:
+ self.subdir_root = os.path.join(self.source_dir, self.subdir)
+ self.cachedir = os.path.join(self.subdir_root, 'packagecache')
+ self.wraps = {} # type: T.Dict[str, PackageDefinition]
+ self.netrc: T.Optional[netrc] = None
+ self.provided_deps = {} # type: T.Dict[str, PackageDefinition]
+ self.provided_programs = {} # type: T.Dict[str, PackageDefinition]
+ self.wrapdb: T.Dict[str, T.Any] = {}
+ self.wrapdb_provided_deps: T.Dict[str, str] = {}
+ self.wrapdb_provided_programs: T.Dict[str, str] = {}
+ self.load_wraps()
+ self.load_netrc()
+ self.load_wrapdb()
+
+ def load_netrc(self) -> None:
+ try:
+ self.netrc = netrc()
+ except FileNotFoundError:
+ return
+ except Exception as e:
+ mlog.warning(f'failed to process netrc file: {e}.', fatal=False)
+
+ def load_wraps(self) -> None:
+ if not os.path.isdir(self.subdir_root):
+ return
+ root, dirs, files = next(os.walk(self.subdir_root))
+ ignore_dirs = {'packagecache', 'packagefiles'}
+ for i in files:
+ if not i.endswith('.wrap'):
+ continue
+ fname = os.path.join(self.subdir_root, i)
+ wrap = PackageDefinition(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)
+ self.wraps[wrap.name] = wrap
+
+ for wrap in self.wraps.values():
+ self.add_wrap(wrap)
+
+ 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}'
+ 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}'
+ raise WrapException(m)
+ self.provided_programs[k] = wrap
+
+ def load_wrapdb(self) -> None:
+ try:
+ with Path(self.subdir_root, 'wrapdb.json').open('r', encoding='utf-8') as f:
+ self.wrapdb = json.load(f)
+ except FileNotFoundError:
+ return
+ for name, info in self.wrapdb.items():
+ self.wrapdb_provided_deps.update({i: name for i in info.get('dependency_names', [])})
+ self.wrapdb_provided_programs.update({i: name for i in info.get('program_names', [])})
+
+ def get_from_wrapdb(self, subp_name: str) -> PackageDefinition:
+ info = self.wrapdb.get(subp_name)
+ if not info:
+ return None
+ self.check_can_download()
+ latest_version = info['versions'][0]
+ version, revision = latest_version.rsplit('-', 1)
+ url = urllib.request.urlopen(f'https://wrapdb.mesonbuild.com/v2/{subp_name}_{version}-{revision}/{subp_name}.wrap')
+ fname = Path(self.subdir_root, f'{subp_name}.wrap')
+ with fname.open('wb') as f:
+ f.write(url.read())
+ mlog.log(f'Installed {subp_name} version {version} revision {revision}')
+ wrap = PackageDefinition(str(fname))
+ self.wraps[wrap.name] = wrap
+ self.add_wrap(wrap)
+ return wrap
+
+ def merge_wraps(self, other_resolver: 'Resolver') -> None:
+ for k, v in other_resolver.wraps.items():
+ self.wraps.setdefault(k, v)
+ for k, v in other_resolver.provided_deps.items():
+ self.provided_deps.setdefault(k, v)
+ for k, v in other_resolver.provided_programs.items():
+ self.provided_programs.setdefault(k, v)
+
+ def find_dep_provider(self, packagename: str) -> T.Tuple[T.Optional[str], T.Optional[str]]:
+ # Python's ini parser converts all key values to lowercase.
+ # Thus the query name must also be in lower case.
+ packagename = packagename.lower()
+ wrap = self.provided_deps.get(packagename)
+ if wrap:
+ dep_var = wrap.provided_deps.get(packagename)
+ return wrap.name, dep_var
+ wrap_name = self.wrapdb_provided_deps.get(packagename)
+ return wrap_name, None
+
+ def get_varname(self, subp_name: str, depname: str) -> T.Optional[str]:
+ wrap = self.wraps.get(subp_name)
+ return wrap.provided_deps.get(depname) if wrap else None
+
+ def find_program_provider(self, names: T.List[str]) -> T.Optional[str]:
+ for name in names:
+ wrap = self.provided_programs.get(name)
+ if wrap:
+ return wrap.name
+ wrap_name = self.wrapdb_provided_programs.get(name)
+ if wrap_name:
+ return wrap_name
+ return None
+
+ def resolve(self, packagename: str, method: str) -> str:
+ self.packagename = packagename
+ self.directory = packagename
+ self.wrap = self.wraps.get(packagename)
+ if not self.wrap:
+ self.wrap = self.get_from_wrapdb(packagename)
+ if not self.wrap:
+ m = f'Neither a subproject directory nor a {self.packagename}.wrap file was found.'
+ raise WrapNotFoundException(m)
+ self.directory = self.wrap.directory
+
+ 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)
+ 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('''\
+ [wrap-redirect]
+ filename = {}
+ '''.format(os.path.relpath(self.wrap.filename, self.subdir_root))))
+ 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)
+
+ if method == 'meson':
+ buildfile = os.path.join(self.dirname, 'meson.build')
+ elif method == 'cmake':
+ buildfile = os.path.join(self.dirname, 'CMakeLists.txt')
+ else:
+ raise WrapException('Only the methods "meson" and "cmake" are supported')
+
+ # The directory is there and has meson.build? Great, use it.
+ if os.path.exists(buildfile):
+ self.validate()
+ return rel_path
+
+ # Check if the subproject is a git submodule
+ self.resolve_git_submodule()
+
+ if os.path.exists(self.dirname):
+ if not os.path.isdir(self.dirname):
+ raise WrapException('Path already exists but is not a directory')
+ else:
+ if self.wrap.type == 'file':
+ self.get_file()
+ else:
+ self.check_can_download()
+ if self.wrap.type == 'git':
+ self.get_git()
+ elif self.wrap.type == "hg":
+ self.get_hg()
+ elif self.wrap.type == "svn":
+ self.get_svn()
+ else:
+ raise WrapException(f'Unknown wrap type {self.wrap.type!r}')
+ try:
+ self.apply_patch()
+ self.apply_diff_files()
+ except Exception:
+ windows_proof_rmtree(self.dirname)
+ raise
+
+ # A meson.build or CMakeLists.txt file is required in the directory
+ if not os.path.exists(buildfile):
+ raise WrapException(f'Subproject exists but has no {os.path.basename(buildfile)} 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
+ # reference.
+ self.wrap.update_hash_cache(self.dirname)
+
+ return rel_path
+
+ def check_can_download(self) -> None:
+ # Don't download subproject data based on wrap file if requested.
+ # Git submodules are ok (see above)!
+ if self.wrap_mode is WrapMode.nodownload:
+ m = 'Automatic wrap-based subproject downloading is disabled'
+ raise WrapException(m)
+
+ def resolve_git_submodule(self) -> bool:
+ # Is git installed? If not, we're probably not in a git repository and
+ # definitely cannot try to conveniently set up a submodule.
+ if not GIT:
+ return False
+ # Does the directory exist? Even uninitialised submodules checkout an
+ # empty directory to work in
+ if not os.path.isdir(self.dirname):
+ return False
+ # Are we in a git repository?
+ ret, out = quiet_git(['rev-parse'], Path(self.dirname).parent)
+ if not ret:
+ return False
+ # Is `dirname` a submodule?
+ ret, out = quiet_git(['submodule', 'status', '.'], self.dirname)
+ if not ret:
+ return False
+ # Submodule has not been added, add it
+ if out.startswith('+'):
+ mlog.warning('git submodule might be out of date')
+ return True
+ elif out.startswith('U'):
+ raise WrapException('git submodule has merge conflicts')
+ # Submodule exists, but is deinitialized or wasn't initialized
+ elif out.startswith('-'):
+ if verbose_git(['submodule', 'update', '--init', '.'], self.dirname):
+ return True
+ raise WrapException('git submodule failed to init')
+ # Submodule looks fine, but maybe it wasn't populated properly. Do a checkout.
+ elif out.startswith(' '):
+ verbose_git(['submodule', 'update', '.'], self.dirname)
+ verbose_git(['checkout', '.'], self.dirname)
+ # Even if checkout failed, try building it anyway and let the user
+ # handle any problems manually.
+ return True
+ elif out == '':
+ # It is not a submodule, just a folder that exists in the main repository.
+ return False
+ raise WrapException(f'Unknown git submodule output: {out!r}')
+
+ def get_file(self) -> None:
+ path = self.get_file_internal('source')
+ extract_dir = self.subdir_root
+ # Some upstreams ship packages that do not have a leading directory.
+ # Create one for them.
+ if 'lead_directory_missing' in self.wrap.values:
+ os.mkdir(self.dirname)
+ extract_dir = self.dirname
+ shutil.unpack_archive(path, extract_dir)
+
+ def get_git(self) -> None:
+ if not GIT:
+ raise WrapException(f'Git program not found, cannot download {self.packagename}.wrap via git.')
+ revno = self.wrap.get('revision')
+ checkout_cmd = ['-c', 'advice.detachedHead=false', 'checkout', revno, '--']
+ is_shallow = False
+ depth_option = [] # type: T.List[str]
+ if self.wrap.values.get('depth', '') != '':
+ is_shallow = True
+ depth_option = ['--depth', self.wrap.values.get('depth')]
+ # for some reason git only allows commit ids to be shallowly fetched by fetch not with clone
+ if is_shallow and self.is_git_full_commit_id(revno):
+ # git doesn't support directly cloning shallowly for commits,
+ # so we follow https://stackoverflow.com/a/43136160
+ verbose_git(['-c', 'init.defaultBranch=meson-dummy-branch', 'init', self.directory], self.subdir_root, check=True)
+ verbose_git(['remote', 'add', 'origin', self.wrap.get('url')], self.dirname, check=True)
+ revno = self.wrap.get('revision')
+ verbose_git(['fetch', *depth_option, 'origin', revno], self.dirname, check=True)
+ verbose_git(checkout_cmd, self.dirname, check=True)
+ if self.wrap.values.get('clone-recursive', '').lower() == 'true':
+ verbose_git(['submodule', 'update', '--init', '--checkout',
+ '--recursive', *depth_option], self.dirname, check=True)
+ push_url = self.wrap.values.get('push-url')
+ if push_url:
+ verbose_git(['remote', 'set-url', '--push', 'origin', push_url], self.dirname, check=True)
+ else:
+ if not is_shallow:
+ verbose_git(['clone', self.wrap.get('url'), self.directory], self.subdir_root, check=True)
+ if revno.lower() != 'head':
+ if not verbose_git(checkout_cmd, self.dirname):
+ verbose_git(['fetch', self.wrap.get('url'), revno], self.dirname, check=True)
+ verbose_git(checkout_cmd, self.dirname, check=True)
+ else:
+ args = ['-c', 'advice.detachedHead=false', 'clone', *depth_option]
+ if revno.lower() != 'head':
+ args += ['--branch', revno]
+ args += [self.wrap.get('url'), self.directory]
+ verbose_git(args, self.subdir_root, check=True)
+ if self.wrap.values.get('clone-recursive', '').lower() == 'true':
+ verbose_git(['submodule', 'update', '--init', '--checkout', '--recursive', *depth_option],
+ self.dirname, check=True)
+ push_url = self.wrap.values.get('push-url')
+ if push_url:
+ verbose_git(['remote', 'set-url', '--push', 'origin', push_url], self.dirname, check=True)
+
+ def validate(self) -> None:
+ # This check is only for subprojects with wraps.
+ if not self.wrap.has_wrap:
+ return
+
+ # Retrieve original hash, if it exists.
+ hashfile = self.wrap.get_hashfile(self.dirname)
+ if os.path.isfile(hashfile):
+ with open(hashfile, 'r', encoding='utf-8') as file:
+ expected_hash = file.read().strip()
+ else:
+ # 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:
+ 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:
+ result = False
+ if len(revno) in {40, 64}: # 40 for sha1, 64 for upcoming sha256
+ result = all(ch in '0123456789AaBbCcDdEeFf' for ch in revno)
+ return result
+
+ def get_hg(self) -> None:
+ revno = self.wrap.get('revision')
+ hg = shutil.which('hg')
+ if not hg:
+ raise WrapException('Mercurial program not found.')
+ subprocess.check_call([hg, 'clone', self.wrap.get('url'),
+ self.directory], cwd=self.subdir_root)
+ if revno.lower() != 'tip':
+ subprocess.check_call([hg, 'checkout', revno],
+ cwd=self.dirname)
+
+ def get_svn(self) -> None:
+ revno = self.wrap.get('revision')
+ svn = shutil.which('svn')
+ if not svn:
+ raise WrapException('SVN program not found.')
+ subprocess.check_call([svn, 'checkout', '-r', revno, self.wrap.get('url'),
+ self.directory], cwd=self.subdir_root)
+
+ def get_netrc_credentials(self, netloc: str) -> T.Optional[T.Tuple[str, str]]:
+ if self.netrc is None or netloc not in self.netrc.hosts:
+ return None
+
+ login, account, password = self.netrc.authenticators(netloc)
+ if account is not None:
+ login = account
+
+ return login, password
+
+ def get_data(self, urlstring: str) -> T.Tuple[str, str]:
+ blocksize = 10 * 1024
+ h = hashlib.sha256()
+ tmpfile = tempfile.NamedTemporaryFile(mode='wb', dir=self.cachedir, delete=False)
+ url = urllib.parse.urlparse(urlstring)
+ if url.hostname and url.hostname.endswith(WHITELIST_SUBDOMAIN):
+ resp = open_wrapdburl(urlstring, allow_insecure=self.allow_insecure, have_opt=self.wrap_frontend)
+ elif WHITELIST_SUBDOMAIN in urlstring:
+ raise WrapException(f'{urlstring} may be a WrapDB-impersonating URL')
+ else:
+ headers = {'User-Agent': f'mesonbuild/{coredata.version}'}
+ creds = self.get_netrc_credentials(url.netloc)
+
+ if creds is not None and '@' not in url.netloc:
+ login, password = creds
+ if url.scheme == 'https':
+ enc_creds = b64encode(f'{login}:{password}'.encode()).decode()
+ headers.update({'Authorization': f'Basic {enc_creds}'})
+ elif url.scheme == 'ftp':
+ urlstring = urllib.parse.urlunparse(url._replace(netloc=f'{login}:{password}@{url.netloc}'))
+ else:
+ mlog.warning('Meson is not going to use netrc credentials for protocols other than https/ftp',
+ fatal=False)
+
+ try:
+ req = urllib.request.Request(urlstring, headers=headers)
+ resp = urllib.request.urlopen(req, timeout=REQ_TIMEOUT)
+ except urllib.error.URLError as e:
+ mlog.log(str(e))
+ raise WrapException(f'could not get {urlstring} is the internet available?')
+ with contextlib.closing(resp) as resp, tmpfile as tmpfile:
+ try:
+ dlsize = int(resp.info()['Content-Length'])
+ except TypeError:
+ dlsize = None
+ if dlsize is None:
+ print('Downloading file of unknown size.')
+ while True:
+ block = resp.read(blocksize)
+ if block == b'':
+ break
+ h.update(block)
+ tmpfile.write(block)
+ hashvalue = h.hexdigest()
+ return hashvalue, tmpfile.name
+ sys.stdout.flush()
+ progress_bar = ProgressBar(bar_type='download', total=dlsize,
+ desc='Downloading')
+ while True:
+ block = resp.read(blocksize)
+ if block == b'':
+ break
+ h.update(block)
+ tmpfile.write(block)
+ progress_bar.update(len(block))
+ progress_bar.close()
+ hashvalue = h.hexdigest()
+ return hashvalue, tmpfile.name
+
+ def check_hash(self, what: str, path: str, hash_required: bool = True) -> None:
+ if what + '_hash' not in self.wrap.values and not hash_required:
+ return
+ expected = self.wrap.get(what + '_hash').lower()
+ h = hashlib.sha256()
+ with open(path, 'rb') as f:
+ h.update(f.read())
+ dhash = h.hexdigest()
+ if dhash != expected:
+ raise WrapException(f'Incorrect hash for {what}:\n {expected} expected\n {dhash} actual.')
+
+ def get_data_with_backoff(self, urlstring: str) -> T.Tuple[str, str]:
+ delays = [1, 2, 4, 8, 16]
+ for d in delays:
+ try:
+ return self.get_data(urlstring)
+ except Exception as e:
+ mlog.warning(f'failed to download with error: {e}. Trying after a delay...', fatal=False)
+ time.sleep(d)
+ return self.get_data(urlstring)
+
+ def download(self, what: str, ofname: str, fallback: bool = False) -> None:
+ self.check_can_download()
+ srcurl = self.wrap.get(what + ('_fallback_url' if fallback else '_url'))
+ mlog.log('Downloading', mlog.bold(self.packagename), what, 'from', mlog.bold(srcurl))
+ try:
+ dhash, tmpfile = self.get_data_with_backoff(srcurl)
+ expected = self.wrap.get(what + '_hash').lower()
+ if dhash != expected:
+ os.remove(tmpfile)
+ raise WrapException(f'Incorrect hash for {what}:\n {expected} expected\n {dhash} actual.')
+ except WrapException:
+ if not fallback:
+ if what + '_fallback_url' in self.wrap.values:
+ return self.download(what, ofname, fallback=True)
+ mlog.log('A fallback URL could be specified using',
+ mlog.bold(what + '_fallback_url'), 'key in the wrap file')
+ raise
+ os.rename(tmpfile, ofname)
+
+ def get_file_internal(self, what: str) -> str:
+ filename = self.wrap.get(what + '_filename')
+ if what + '_url' in self.wrap.values:
+ cache_path = os.path.join(self.cachedir, filename)
+
+ if os.path.exists(cache_path):
+ self.check_hash(what, cache_path)
+ mlog.log('Using', mlog.bold(self.packagename), what, 'from cache.')
+ return cache_path
+
+ os.makedirs(self.cachedir, exist_ok=True)
+ self.download(what, cache_path)
+ return cache_path
+ else:
+ path = Path(self.wrap.filesdir) / filename
+
+ if not path.exists():
+ raise WrapException(f'File "{path}" does not exist')
+ self.check_hash(what, path.as_posix(), hash_required=False)
+
+ return path.as_posix()
+
+ def apply_patch(self) -> 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"'
+ raise WrapException(m)
+ if 'patch_filename' in self.wrap.values:
+ path = self.get_file_internal('patch')
+ try:
+ shutil.unpack_archive(path, self.subdir_root)
+ except Exception:
+ with tempfile.TemporaryDirectory() as workdir:
+ shutil.unpack_archive(path, workdir)
+ self.copy_tree(workdir, self.subdir_root)
+ elif 'patch_directory' in self.wrap.values:
+ patch_dir = self.wrap.values['patch_directory']
+ src_dir = os.path.join(self.wrap.filesdir, patch_dir)
+ if not os.path.isdir(src_dir):
+ raise WrapException(f'patch directory does not exist: {patch_dir}')
+ self.copy_tree(src_dir, self.dirname)
+
+ def apply_diff_files(self) -> None:
+ for filename in self.wrap.diff_files:
+ mlog.log(f'Applying diff file "{filename}"')
+ path = Path(self.wrap.filesdir) / filename
+ if not path.exists():
+ raise WrapException(f'Diff file "{path}" does not exist')
+ relpath = os.path.relpath(str(path), self.dirname)
+ if PATCH:
+ cmd = [PATCH, '-f', '-p1', '-i', relpath]
+ elif GIT:
+ # If the `patch` command is not available, fall back to `git
+ # apply`. The `--work-tree` is necessary in case we're inside a
+ # Git repository: by default, Git will try to apply the patch to
+ # the repository root.
+ cmd = [GIT, '--work-tree', '.', 'apply', '-p1', relpath]
+ else:
+ raise WrapException('Missing "patch" or "git" commands to apply diff files')
+
+ p, out, _ = Popen_safe(cmd, cwd=self.dirname, stderr=subprocess.STDOUT)
+ if p.returncode != 0:
+ mlog.log(out.strip())
+ raise WrapException(f'Failed to apply diff file "{filename}"')
+
+ def copy_tree(self, root_src_dir: str, root_dst_dir: str) -> None:
+ """
+ Copy directory tree. Overwrites also read only files.
+ """
+ for src_dir, _, files in os.walk(root_src_dir):
+ dst_dir = src_dir.replace(root_src_dir, root_dst_dir, 1)
+ if not os.path.exists(dst_dir):
+ os.makedirs(dst_dir)
+ for file_ in files:
+ src_file = os.path.join(src_dir, file_)
+ dst_file = os.path.join(dst_dir, file_)
+ if os.path.exists(dst_file):
+ try:
+ os.remove(dst_file)
+ except PermissionError:
+ os.chmod(dst_file, stat.S_IWUSR)
+ os.remove(dst_file)
+ shutil.copy2(src_file, dst_dir)
diff --git a/mesonbuild/wrap/wraptool.py b/mesonbuild/wrap/wraptool.py
new file mode 100644
index 0000000..c009aa1
--- /dev/null
+++ b/mesonbuild/wrap/wraptool.py
@@ -0,0 +1,231 @@
+# Copyright 2015-2016 The Meson development team
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from __future__ import annotations
+
+import sys, os
+import configparser
+import shutil
+import typing as T
+
+from glob import glob
+from .wrap import (open_wrapdburl, WrapException, get_releases, get_releases_data,
+ update_wrap_file, parse_patch_url)
+from pathlib import Path
+
+from .. import mesonlib, msubprojects
+
+if T.TYPE_CHECKING:
+ import argparse
+
+def add_arguments(parser: 'argparse.ArgumentParser') -> None:
+ subparsers = parser.add_subparsers(title='Commands', dest='command')
+ subparsers.required = True
+
+ p = subparsers.add_parser('list', help='show all available projects')
+ p.add_argument('--allow-insecure', default=False, action='store_true',
+ help='Allow insecure server connections.')
+ p.set_defaults(wrap_func=list_projects)
+
+ p = subparsers.add_parser('search', help='search the db by name')
+ p.add_argument('--allow-insecure', default=False, action='store_true',
+ help='Allow insecure server connections.')
+ p.add_argument('name')
+ p.set_defaults(wrap_func=search)
+
+ p = subparsers.add_parser('install', help='install the specified project')
+ p.add_argument('--allow-insecure', default=False, action='store_true',
+ help='Allow insecure server connections.')
+ p.add_argument('name')
+ p.set_defaults(wrap_func=install)
+
+ p = msubprojects.add_wrap_update_parser(subparsers)
+ p.set_defaults(wrap_func=msubprojects.run)
+
+ p = subparsers.add_parser('info', help='show available versions of a project')
+ p.add_argument('--allow-insecure', default=False, action='store_true',
+ help='Allow insecure server connections.')
+ p.add_argument('name')
+ p.set_defaults(wrap_func=info)
+
+ p = subparsers.add_parser('status', help='show installed and available versions of your projects')
+ p.add_argument('--allow-insecure', default=False, action='store_true',
+ help='Allow insecure server connections.')
+ p.set_defaults(wrap_func=status)
+
+ p = subparsers.add_parser('promote', help='bring a subsubproject up to the master project')
+ p.add_argument('project_path')
+ p.set_defaults(wrap_func=promote)
+
+ p = subparsers.add_parser('update-db', help='Update list of projects available in WrapDB (Since 0.61.0)')
+ p.add_argument('--allow-insecure', default=False, action='store_true',
+ help='Allow insecure server connections.')
+ p.set_defaults(wrap_func=update_db)
+
+def list_projects(options: 'argparse.Namespace') -> None:
+ releases = get_releases(options.allow_insecure)
+ for p in releases.keys():
+ print(p)
+
+def search(options: 'argparse.Namespace') -> None:
+ name = options.name
+ releases = get_releases(options.allow_insecure)
+ for p, info in releases.items():
+ if p.find(name) != -1:
+ print(p)
+ else:
+ for dep in info.get('dependency_names', []):
+ if dep.find(name) != -1:
+ print(f'Dependency {dep} found in wrap {p}')
+
+def get_latest_version(name: str, allow_insecure: bool) -> T.Tuple[str, str]:
+ releases = get_releases(allow_insecure)
+ info = releases.get(name)
+ if not info:
+ raise WrapException(f'Wrap {name} not found in wrapdb')
+ latest_version = info['versions'][0]
+ version, revision = latest_version.rsplit('-', 1)
+ return version, revision
+
+def install(options: 'argparse.Namespace') -> None:
+ name = options.name
+ if not os.path.isdir('subprojects'):
+ raise SystemExit('Subprojects dir not found. Run this script in your source root directory.')
+ if os.path.isdir(os.path.join('subprojects', name)):
+ raise SystemExit('Subproject directory for this project already exists.')
+ wrapfile = os.path.join('subprojects', name + '.wrap')
+ if os.path.exists(wrapfile):
+ raise SystemExit('Wrap file already exists.')
+ (version, revision) = get_latest_version(name, options.allow_insecure)
+ url = open_wrapdburl(f'https://wrapdb.mesonbuild.com/v2/{name}_{version}-{revision}/{name}.wrap', options.allow_insecure, True)
+ with open(wrapfile, 'wb') as f:
+ f.write(url.read())
+ print(f'Installed {name} version {version} revision {revision}')
+
+def get_current_version(wrapfile: str) -> T.Tuple[str, str, str, str, T.Optional[str]]:
+ cp = configparser.ConfigParser(interpolation=None)
+ cp.read(wrapfile)
+ try:
+ wrap_data = cp['wrap-file']
+ except KeyError:
+ raise WrapException('Not a wrap-file, cannot have come from the wrapdb')
+ try:
+ patch_url = wrap_data['patch_url']
+ except KeyError:
+ # We assume a wrap without a patch_url is probably just an pointer to upstream's
+ # build files. The version should be in the tarball filename, even if it isn't
+ # purely guaranteed. The wrapdb revision should be 1 because it just needs uploading once.
+ branch = mesonlib.search_version(wrap_data['source_filename'])
+ revision, patch_filename = '1', None
+ else:
+ branch, revision = parse_patch_url(patch_url)
+ patch_filename = wrap_data['patch_filename']
+ return branch, revision, wrap_data['directory'], wrap_data['source_filename'], patch_filename
+
+def update(options: 'argparse.Namespace') -> None:
+ name = options.name
+ if not os.path.isdir('subprojects'):
+ raise SystemExit('Subprojects dir not found. Run this command in your source root directory.')
+ wrapfile = os.path.join('subprojects', name + '.wrap')
+ if not os.path.exists(wrapfile):
+ raise SystemExit('Project ' + name + ' is not in use.')
+ (branch, revision, subdir, src_file, patch_file) = get_current_version(wrapfile)
+ (new_branch, new_revision) = get_latest_version(name, options.allow_insecure)
+ if new_branch == branch and new_revision == revision:
+ print('Project ' + name + ' is already up to date.')
+ raise SystemExit
+ update_wrap_file(wrapfile, name, new_branch, new_revision, options.allow_insecure)
+ shutil.rmtree(os.path.join('subprojects', subdir), ignore_errors=True)
+ try:
+ os.unlink(os.path.join('subprojects/packagecache', src_file))
+ except FileNotFoundError:
+ pass
+ if patch_file is not None:
+ try:
+ os.unlink(os.path.join('subprojects/packagecache', patch_file))
+ except FileNotFoundError:
+ pass
+ print(f'Updated {name} version {new_branch} revision {new_revision}')
+
+def info(options: 'argparse.Namespace') -> None:
+ name = options.name
+ releases = get_releases(options.allow_insecure)
+ info = releases.get(name)
+ if not info:
+ raise WrapException(f'Wrap {name} not found in wrapdb')
+ print(f'Available versions of {name}:')
+ for v in info['versions']:
+ print(' ', v)
+
+def do_promotion(from_path: str, spdir_name: str) -> None:
+ if os.path.isfile(from_path):
+ assert from_path.endswith('.wrap')
+ shutil.copy(from_path, spdir_name)
+ elif os.path.isdir(from_path):
+ sproj_name = os.path.basename(from_path)
+ outputdir = os.path.join(spdir_name, sproj_name)
+ if os.path.exists(outputdir):
+ raise SystemExit(f'Output dir {outputdir} already exists. Will not overwrite.')
+ shutil.copytree(from_path, outputdir, ignore=shutil.ignore_patterns('subprojects'))
+
+def promote(options: 'argparse.Namespace') -> None:
+ argument = options.project_path
+ spdir_name = 'subprojects'
+ sprojs = mesonlib.detect_subprojects(spdir_name)
+
+ # check if the argument is a full path to a subproject directory or wrap file
+ system_native_path_argument = argument.replace('/', os.sep)
+ for matches in sprojs.values():
+ if system_native_path_argument in matches:
+ do_promotion(system_native_path_argument, spdir_name)
+ return
+
+ # otherwise the argument is just a subproject basename which must be unambiguous
+ if argument not in sprojs:
+ raise SystemExit(f'Subproject {argument} not found in directory tree.')
+ matches = sprojs[argument]
+ if len(matches) > 1:
+ print(f'There is more than one version of {argument} in tree. Please specify which one to promote:\n', file=sys.stderr)
+ for s in matches:
+ print(s, file=sys.stderr)
+ raise SystemExit(1)
+ do_promotion(matches[0], spdir_name)
+
+def status(options: 'argparse.Namespace') -> None:
+ print('Subproject status')
+ for w in glob('subprojects/*.wrap'):
+ name = os.path.basename(w)[:-5]
+ try:
+ (latest_branch, latest_revision) = get_latest_version(name, options.allow_insecure)
+ except Exception:
+ print('', name, 'not available in wrapdb.', file=sys.stderr)
+ continue
+ try:
+ (current_branch, current_revision, _, _, _) = get_current_version(w)
+ except Exception:
+ print('', name, 'Wrap file not from wrapdb.', file=sys.stderr)
+ continue
+ if current_branch == latest_branch and current_revision == latest_revision:
+ print('', name, f'up to date. Branch {current_branch}, revision {current_revision}.')
+ else:
+ print('', name, f'not up to date. Have {current_branch} {current_revision}, but {latest_branch} {latest_revision} is available.')
+
+def update_db(options: 'argparse.Namespace') -> None:
+ data = get_releases_data(options.allow_insecure)
+ Path('subprojects').mkdir(exist_ok=True)
+ with Path('subprojects/wrapdb.json').open('wb') as f:
+ f.write(data)
+
+def run(options: 'argparse.Namespace') -> int:
+ options.wrap_func(options)
+ return 0