aboutsummaryrefslogtreecommitdiff
path: root/mesonbuild/mdevenv.py
blob: 624fec99ee52407367d954e668d03f85136e39ee (plain)
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
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

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