diff options
Diffstat (limited to 'mesonbuild/mdevenv.py')
-rw-r--r-- | mesonbuild/mdevenv.py | 215 |
1 files changed, 215 insertions, 0 deletions
diff --git a/mesonbuild/mdevenv.py b/mesonbuild/mdevenv.py new file mode 100644 index 0000000..05779e8 --- /dev/null +++ b/mesonbuild/mdevenv.py @@ -0,0 +1,215 @@ +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', action='store_true', + help='Only print required environment (Since 0.62.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: bool) -> 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 else os.environ.copy() + varnames = set() + for i in itertools.chain(b.devenv, {extra_env}): + env = i.get_env(env, dump) + 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 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 + devenv, varnames = get_env(b, options.dump) + if options.dump: + if options.devcmd: + raise MesonException('--dump option does not allow running other command.') + for name in varnames: + print(f'{name}="{devenv[name]}"') + print(f'export {name}') + 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]}') |