|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
import os, subprocess
|
|
|
|
import argparse
|
|
|
|
import tempfile
|
|
|
|
import shutil
|
|
|
|
import itertools
|
|
|
|
import typing as T
|
|
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
from . import build, minstall
|
|
|
|
from .mesonlib import (EnvironmentVariables, MesonException, is_windows, setup_vsenv, OptionKey,
|
|
|
|
get_wine_shortpath, MachineChoice)
|
|
|
|
from . import mlog
|
|
|
|
|
|
|
|
|
|
|
|
if T.TYPE_CHECKING:
|
|
|
|
from .backend.backends import InstallData
|
|
|
|
|
|
|
|
POWERSHELL_EXES = {'pwsh.exe', 'powershell.exe'}
|
|
|
|
|
|
|
|
# Note: when adding arguments, please also add them to the completion
|
|
|
|
# scripts in $MESONSRC/data/shell-completions/
|
|
|
|
def add_arguments(parser: argparse.ArgumentParser) -> None:
|
|
|
|
parser.add_argument('-C', dest='builddir', type=Path, default='.',
|
|
|
|
help='Path to build directory')
|
|
|
|
parser.add_argument('--workdir', '-w', type=Path, default=None,
|
|
|
|
help='Directory to cd into before running (default: builddir, Since 1.0.0)')
|
|
|
|
parser.add_argument('--dump', nargs='?', const=True,
|
|
|
|
help='Only print required environment (Since 0.62.0) ' +
|
|
|
|
'Takes an optional file path (Since 1.1.0)')
|
|
|
|
parser.add_argument('--dump-format', default='export',
|
|
|
|
choices=['sh', 'export', 'vscode'],
|
|
|
|
help='Format used with --dump (Since 1.1.0)')
|
|
|
|
parser.add_argument('devcmd', nargs=argparse.REMAINDER, metavar='command',
|
|
|
|
help='Command to run in developer environment (default: interactive shell)')
|
|
|
|
|
|
|
|
def get_windows_shell() -> T.Optional[str]:
|
|
|
|
mesonbuild = Path(__file__).parent
|
|
|
|
script = mesonbuild / 'scripts' / 'cmd_or_ps.ps1'
|
|
|
|
for shell in POWERSHELL_EXES:
|
|
|
|
try:
|
|
|
|
command = [shell, '-noprofile', '-executionpolicy', 'bypass', '-file', str(script)]
|
|
|
|
result = subprocess.check_output(command)
|
|
|
|
return result.decode().strip()
|
|
|
|
except (subprocess.CalledProcessError, OSError):
|
|
|
|
pass
|
|
|
|
return None
|
|
|
|
|
|
|
|
def reduce_winepath(env: T.Dict[str, str]) -> None:
|
|
|
|
winepath = env.get('WINEPATH')
|
|
|
|
if not winepath:
|
|
|
|
return
|
|
|
|
winecmd = shutil.which('wine64') or shutil.which('wine')
|
|
|
|
if not winecmd:
|
|
|
|
return
|
|
|
|
env['WINEPATH'] = get_wine_shortpath([winecmd], winepath.split(';'))
|
|
|
|
mlog.log('Meson detected wine and has set WINEPATH accordingly')
|
|
|
|
|
|
|
|
def get_env(b: build.Build, dump_fmt: T.Optional[str]) -> T.Tuple[T.Dict[str, str], T.Set[str]]:
|
|
|
|
extra_env = EnvironmentVariables()
|
|
|
|
extra_env.set('MESON_DEVENV', ['1'])
|
|
|
|
extra_env.set('MESON_PROJECT_NAME', [b.project_name])
|
|
|
|
|
|
|
|
sysroot = b.environment.properties[MachineChoice.HOST].get_sys_root()
|
|
|
|
if sysroot:
|
|
|
|
extra_env.set('QEMU_LD_PREFIX', [sysroot])
|
|
|
|
|
|
|
|
env = {} if dump_fmt else os.environ.copy()
|
|
|
|
default_fmt = '${0}' if dump_fmt in {'sh', 'export'} else None
|
|
|
|
varnames = set()
|
|
|
|
for i in itertools.chain(b.devenv, {extra_env}):
|
|
|
|
env = i.get_env(env, default_fmt)
|
|
|
|
varnames |= i.get_names()
|
|
|
|
|
|
|
|
reduce_winepath(env)
|
|
|
|
|
|
|
|
return env, varnames
|
|
|
|
|
|
|
|
def bash_completion_files(b: build.Build, install_data: 'InstallData') -> T.List[str]:
|
|
|
|
from .dependencies.pkgconfig import PkgConfigDependency
|
|
|
|
result = []
|
|
|
|
dep = PkgConfigDependency('bash-completion', b.environment,
|
|
|
|
{'required': False, 'silent': True, 'version': '>=2.10'})
|
|
|
|
if dep.found():
|
|
|
|
prefix = b.environment.coredata.get_option(OptionKey('prefix'))
|
|
|
|
assert isinstance(prefix, str), 'for mypy'
|
|
|
|
datadir = b.environment.coredata.get_option(OptionKey('datadir'))
|
|
|
|
assert isinstance(datadir, str), 'for mypy'
|
|
|
|
datadir_abs = os.path.join(prefix, datadir)
|
dependencies: allow get_variable to define multiple pkgconfig defines
It was previously impossible to do this:
```
dep.get_pkgconfig_variable(
'foo',
define_variable: ['prefix', '/usr', 'datadir', '/usr/share'],
)
```
since get_pkgconfig_variable mandated exactly two (if any) arguments.
However, you could do this:
```
dep.get_variable(
'foo',
pkgconfig_define: ['prefix', '/usr', 'datadir', '/usr/share'],
)
```
It would silently do the wrong thing, by defining "prefix" as
`/usr=datadir=/usr/share`, which might not "matter" if only datadir was
used in the "foo" variable as the unmodified value might be adequate.
The actual intention of anyone writing such a meson.build is that they
aren't sure whether the .pc file uses ${prefix} or ${datadir} (or which
one gets used, might have changed between versions of that .pc file,
even).
A recent refactor made this into a hard error, which broke some projects
that were doing this and inadvertently depending on some .pc file that
only used the second variable. (This was "fine" since the result was
essentially meaningful, and even resulted in behavior identical to the
intended behavior if both projects were installed into the same prefix
-- in which case there's nothing to remap.)
Re-allow this. There are two ways we could re-allow this:
- ignore it with a warning
- add a new feature to allow actually doing this
Since the use case which triggered this bug actually has a pretty good
reason to want to do this, it makes sense to add the new feature.
Fixes https://bugs.gentoo.org/916576
Fixes https://github.com/containers/bubblewrap/issues/609
1 year ago
|
|
|
completionsdir = dep.get_variable(pkgconfig='completionsdir', pkgconfig_define=(('datadir', datadir_abs),))
|
|
|
|
assert isinstance(completionsdir, str), 'for mypy'
|
|
|
|
completionsdir_path = Path(completionsdir)
|
|
|
|
for f in install_data.data:
|
|
|
|
if completionsdir_path in Path(f.install_path).parents:
|
|
|
|
result.append(f.path)
|
|
|
|
return result
|
|
|
|
|
|
|
|
def add_gdb_auto_load(autoload_path: Path, gdb_helper: str, fname: Path) -> None:
|
|
|
|
# Copy or symlink the GDB helper into our private directory tree
|
|
|
|
destdir = autoload_path / fname.parent
|
|
|
|
destdir.mkdir(parents=True, exist_ok=True)
|
|
|
|
try:
|
|
|
|
if is_windows():
|
|
|
|
shutil.copy(gdb_helper, str(destdir / os.path.basename(gdb_helper)))
|
|
|
|
else:
|
|
|
|
os.symlink(gdb_helper, str(destdir / os.path.basename(gdb_helper)))
|
|
|
|
except (FileExistsError, shutil.SameFileError):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def write_gdb_script(privatedir: Path, install_data: 'InstallData', workdir: Path) -> None:
|
|
|
|
if not shutil.which('gdb'):
|
|
|
|
return
|
|
|
|
bdir = privatedir.parent
|
|
|
|
autoload_basedir = privatedir / 'gdb-auto-load'
|
|
|
|
autoload_path = Path(autoload_basedir, *bdir.parts[1:])
|
|
|
|
have_gdb_helpers = False
|
|
|
|
for d in install_data.data:
|
|
|
|
if d.path.endswith('-gdb.py') or d.path.endswith('-gdb.gdb') or d.path.endswith('-gdb.scm'):
|
|
|
|
# This GDB helper is made for a specific shared library, search if
|
|
|
|
# we have it in our builddir.
|
|
|
|
libname = Path(d.path).name.rsplit('-', 1)[0]
|
|
|
|
for t in install_data.targets:
|
|
|
|
path = Path(t.fname)
|
|
|
|
if path.name == libname:
|
|
|
|
add_gdb_auto_load(autoload_path, d.path, path)
|
|
|
|
have_gdb_helpers = True
|
|
|
|
if have_gdb_helpers:
|
|
|
|
gdbinit_line = f'add-auto-load-scripts-directory {autoload_basedir}\n'
|
|
|
|
gdbinit_path = bdir / '.gdbinit'
|
|
|
|
first_time = False
|
|
|
|
try:
|
|
|
|
with gdbinit_path.open('r+', encoding='utf-8') as f:
|
|
|
|
if gdbinit_line not in f.readlines():
|
|
|
|
f.write(gdbinit_line)
|
|
|
|
first_time = True
|
|
|
|
except FileNotFoundError:
|
|
|
|
gdbinit_path.write_text(gdbinit_line, encoding='utf-8')
|
|
|
|
first_time = True
|
|
|
|
if first_time:
|
|
|
|
gdbinit_path = gdbinit_path.resolve()
|
|
|
|
workdir_path = workdir.resolve()
|
|
|
|
rel_path = gdbinit_path.relative_to(workdir_path)
|
|
|
|
mlog.log('Meson detected GDB helpers and added config in', mlog.bold(str(rel_path)))
|
|
|
|
mlog.log('To load it automatically you might need to:')
|
|
|
|
mlog.log(' - Add', mlog.bold(f'add-auto-load-safe-path {gdbinit_path.parent}'),
|
|
|
|
'in', mlog.bold('~/.gdbinit'))
|
|
|
|
if gdbinit_path.parent != workdir_path:
|
|
|
|
mlog.log(' - Change current workdir to', mlog.bold(str(rel_path.parent)),
|
|
|
|
'or use', mlog.bold(f'--init-command {rel_path}'))
|
|
|
|
|
|
|
|
def dump(devenv: T.Dict[str, str], varnames: T.Set[str], dump_format: T.Optional[str], output: T.Optional[T.TextIO] = None) -> None:
|
|
|
|
for name in varnames:
|
|
|
|
print(f'{name}="{devenv[name]}"', file=output)
|
|
|
|
if dump_format == 'export':
|
|
|
|
print(f'export {name}', file=output)
|
|
|
|
|
|
|
|
def run(options: argparse.Namespace) -> int:
|
|
|
|
privatedir = Path(options.builddir) / 'meson-private'
|
|
|
|
buildfile = privatedir / 'build.dat'
|
|
|
|
if not buildfile.is_file():
|
|
|
|
raise MesonException(f'Directory {options.builddir!r} does not seem to be a Meson build directory.')
|
|
|
|
b = build.load(options.builddir)
|
|
|
|
workdir = options.workdir or options.builddir
|
|
|
|
|
|
|
|
need_vsenv = T.cast('bool', b.environment.coredata.get_option(OptionKey('vsenv')))
|
|
|
|
setup_vsenv(need_vsenv) # Call it before get_env to get vsenv vars as well
|
|
|
|
dump_fmt = options.dump_format if options.dump else None
|
|
|
|
devenv, varnames = get_env(b, dump_fmt)
|
|
|
|
if options.dump:
|
|
|
|
if options.devcmd:
|
|
|
|
raise MesonException('--dump option does not allow running other command.')
|
|
|
|
if options.dump is True:
|
|
|
|
dump(devenv, varnames, dump_fmt)
|
|
|
|
else:
|
|
|
|
with open(options.dump, "w", encoding='utf-8') as output:
|
|
|
|
dump(devenv, varnames, dump_fmt, output)
|
|
|
|
return 0
|
|
|
|
|
|
|
|
if b.environment.need_exe_wrapper():
|
|
|
|
m = 'An executable wrapper could be required'
|
|
|
|
exe_wrapper = b.environment.get_exe_wrapper()
|
|
|
|
if exe_wrapper:
|
|
|
|
cmd = ' '.join(exe_wrapper.get_command())
|
|
|
|
m += f': {cmd}'
|
|
|
|
mlog.log(m)
|
|
|
|
|
|
|
|
install_data = minstall.load_install_data(str(privatedir / 'install.dat'))
|
|
|
|
write_gdb_script(privatedir, install_data, workdir)
|
|
|
|
|
|
|
|
args = options.devcmd
|
|
|
|
if not args:
|
|
|
|
prompt_prefix = f'[{b.project_name}]'
|
|
|
|
shell_env = os.environ.get("SHELL")
|
|
|
|
# Prefer $SHELL in a MSYS2 bash despite it being Windows
|
|
|
|
if shell_env and os.path.exists(shell_env):
|
|
|
|
args = [shell_env]
|
|
|
|
elif is_windows():
|
|
|
|
shell = get_windows_shell()
|
|
|
|
if not shell:
|
|
|
|
mlog.warning('Failed to determine Windows shell, fallback to cmd.exe')
|
|
|
|
if shell in POWERSHELL_EXES:
|
|
|
|
args = [shell, '-NoLogo', '-NoExit']
|
|
|
|
prompt = f'function global:prompt {{ "{prompt_prefix} PS " + $PWD + "> "}}'
|
|
|
|
args += ['-Command', prompt]
|
|
|
|
else:
|
|
|
|
args = [os.environ.get("COMSPEC", r"C:\WINDOWS\system32\cmd.exe")]
|
|
|
|
args += ['/k', f'prompt {prompt_prefix} $P$G']
|
|
|
|
else:
|
|
|
|
args = [os.environ.get("SHELL", os.path.realpath("/bin/sh"))]
|
|
|
|
if "bash" in args[0]:
|
|
|
|
# Let the GC remove the tmp file
|
|
|
|
tmprc = tempfile.NamedTemporaryFile(mode='w')
|
|
|
|
tmprc.write('[ -e ~/.bashrc ] && . ~/.bashrc\n')
|
|
|
|
if not os.environ.get("MESON_DISABLE_PS1_OVERRIDE"):
|
|
|
|
tmprc.write(f'export PS1="{prompt_prefix} $PS1"\n')
|
|
|
|
for f in bash_completion_files(b, install_data):
|
|
|
|
tmprc.write(f'. "{f}"\n')
|
|
|
|
tmprc.flush()
|
|
|
|
args.append("--rcfile")
|
|
|
|
args.append(tmprc.name)
|
|
|
|
else:
|
|
|
|
# Try to resolve executable using devenv's PATH
|
|
|
|
abs_path = shutil.which(args[0], path=devenv.get('PATH', None))
|
|
|
|
args[0] = abs_path or args[0]
|
|
|
|
|
|
|
|
try:
|
|
|
|
return subprocess.call(args, close_fds=False,
|
|
|
|
env=devenv,
|
|
|
|
cwd=workdir)
|
|
|
|
except subprocess.CalledProcessError as e:
|
|
|
|
return e.returncode
|
|
|
|
except FileNotFoundError:
|
|
|
|
raise MesonException(f'Command not found: {args[0]}')
|