blob: 9f3d1b973d2884dd5eb18b713a11ec846b3fdb3e [file] [log] [blame]
from __future__ import annotations
import os, subprocess
import argparse
import tempfile
import shutil
import itertools
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
import typing as T
if T.TYPE_CHECKING:
from .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)
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]}')