|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
import os, subprocess
|
|
|
|
import argparse
|
|
|
|
import tempfile
|
|
|
|
import shutil
|
|
|
|
import itertools
|
|
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
from . import build, minstall, dependencies
|
|
|
|
from .mesonlib import (MesonException, is_windows, setup_vsenv, OptionKey,
|
|
|
|
get_wine_shortpath, MachineChoice)
|
|
|
|
from . import mlog
|
|
|
|
|
|
|
|
import typing as T
|
|
|
|
if T.TYPE_CHECKING:
|
|
|
|
from .backends import InstallData
|
|
|
|
|
|
|
|
POWERSHELL_EXES = {'pwsh.exe', 'powershell.exe'}
|
|
|
|
|
|
|
|
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 = build.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]:
|
|
|
|
result = []
|
|
|
|
dep = dependencies.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)
|
|
|
|
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
|
|
|
|
|
|
|
|
setup_vsenv(b.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]}')
|