1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277
|
from __future__ import annotations
import os, subprocess
import argparse
import tempfile
import shutil
import sys
import itertools
import signal
import typing as T
from pathlib import Path
from . import build, minstall
from .mesonlib import (EnvironmentVariables, MesonException, join_args, is_windows, setup_vsenv,
get_wine_shortpath, MachineChoice, relpath, is_osx)
from .options import OptionKey
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.optstore.get_value_for(OptionKey('prefix'))
assert isinstance(prefix, str), 'for mypy'
datadir = b.environment.coredata.optstore.get_value_for(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 = Path(relpath(gdbinit_path, 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 macos_sip_enabled() -> bool:
if not is_osx():
return False
ret = subprocess.run(["csrutil", "status"], text=True, capture_output=True, encoding='utf-8')
if not ret.stdout:
return True
return 'enabled' in ret.stdout
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.optstore.get_value_for(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}]'
if os.environ.get("MESON_DISABLE_PS1_OVERRIDE"):
prompt_prefix = None
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']
if prompt_prefix:
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 prompt_prefix:
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)
elif args[0].endswith('fish'):
# Ignore SIGINT while using fish as the shell to make it behave
# like other shells such as bash and zsh.
# See: https://gitlab.freedesktop.org/gstreamer/gst-build/issues/18
signal.signal(signal.SIGINT, lambda _, __: True)
if prompt_prefix:
args.append('--init-command')
prompt_cmd = f'''functions --copy fish_prompt original_fish_prompt
function fish_prompt
echo -n '[{prompt_prefix}] '(original_fish_prompt)
end'''
args.append(prompt_cmd)
elif args[0].endswith('zsh'):
# Let the GC remove the tmp file
tmpdir = tempfile.TemporaryDirectory()
with open(os.path.join(tmpdir.name, '.zshrc'), 'w') as zshrc: # pylint: disable=unspecified-encoding
zshrc.write('[ -e ~/.zshrc ] && . ~/.zshrc\n')
if prompt_prefix:
zshrc.write(f'export PROMPT="[{prompt_prefix}] $PROMPT"\n')
devenv['ZDOTDIR'] = tmpdir.name
if 'DYLD_LIBRARY_PATH' in devenv and macos_sip_enabled():
mlog.warning('macOS System Integrity Protection is enabled: DYLD_LIBRARY_PATH cannot be set in the subshell')
mlog.warning('To fix that, use `meson devenv --dump dev.env && source dev.env`')
del devenv['DYLD_LIBRARY_PATH']
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:
if is_windows():
# execvpe doesn't return exit code on Windows
# see https://github.com/python/cpython/issues/63323
result = subprocess.run(args, env=devenv, cwd=workdir)
sys.exit(result.returncode)
else:
os.chdir(workdir)
os.execvpe(args[0], args, env=devenv)
except FileNotFoundError:
raise MesonException(f'Command not found: {args[0]}')
except OSError as e:
raise MesonException(f'Command `{join_args(args)}` failed to execute: {e}')
|