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]}')