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 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368
|
# SPDX-License-Identifier: Apache-2.0
# Copyright 2013-2020 The Meson development team
from __future__ import annotations
"""Representations and logic for External and Internal Programs."""
import functools
import os
import shutil
import stat
import sys
import re
import typing as T
from pathlib import Path
from . import mesonlib
from . import mlog
from .mesonlib import MachineChoice, OrderedSet
if T.TYPE_CHECKING:
from .environment import Environment
from .interpreter import Interpreter
class ExternalProgram(mesonlib.HoldableObject):
"""A program that is found on the system."""
windows_exts = ('exe', 'msc', 'com', 'bat', 'cmd')
for_machine = MachineChoice.BUILD
def __init__(self, name: str, command: T.Optional[T.List[str]] = None,
silent: bool = False, search_dir: T.Optional[str] = None,
extra_search_dirs: T.Optional[T.List[str]] = None):
self.name = name
self.path: T.Optional[str] = None
self.cached_version: T.Optional[str] = None
self.version_arg = '--version'
if command is not None:
self.command = mesonlib.listify(command)
if mesonlib.is_windows():
cmd = self.command[0]
args = self.command[1:]
# Check whether the specified cmd is a path to a script, in
# which case we need to insert the interpreter. If not, try to
# use it as-is.
ret = self._shebang_to_cmd(cmd)
if ret:
self.command = ret + args
else:
self.command = [cmd] + args
else:
all_search_dirs = [search_dir]
if extra_search_dirs:
all_search_dirs += extra_search_dirs
for d in all_search_dirs:
self.command = self._search(name, d)
if self.found():
break
if self.found():
# Set path to be the last item that is actually a file (in order to
# skip options in something like ['python', '-u', 'file.py']. If we
# can't find any components, default to the last component of the path.
for arg in reversed(self.command):
if arg is not None and os.path.isfile(arg):
self.path = arg
break
else:
self.path = self.command[-1]
if not silent:
# ignore the warning because derived classes never call this __init__
# method, and thus only the found() method of this class is ever executed
if self.found(): # lgtm [py/init-calls-subclass]
mlog.log('Program', mlog.bold(name), 'found:', mlog.green('YES'),
'(%s)' % ' '.join(self.command))
else:
mlog.log('Program', mlog.bold(name), 'found:', mlog.red('NO'))
def summary_value(self) -> T.Union[str, mlog.AnsiDecorator]:
if not self.found():
return mlog.red('NO')
return self.path
def __repr__(self) -> str:
r = '<{} {!r} -> {!r}>'
return r.format(self.__class__.__name__, self.name, self.command)
def description(self) -> str:
'''Human friendly description of the command'''
return ' '.join(self.command)
def get_version(self, interpreter: T.Optional['Interpreter'] = None) -> str:
if not self.cached_version:
raw_cmd = self.get_command() + [self.version_arg]
if interpreter:
res = interpreter.run_command_impl((self, [self.version_arg]),
{'capture': True,
'check': True,
'env': mesonlib.EnvironmentVariables()},
True)
o, e = res.stdout, res.stderr
else:
p, o, e = mesonlib.Popen_safe(raw_cmd)
if p.returncode != 0:
cmd_str = mesonlib.join_args(raw_cmd)
raise mesonlib.MesonException(f'Command {cmd_str!r} failed with status {p.returncode}.')
output = o.strip()
if not output:
output = e.strip()
match = re.search(r'([0-9][0-9\.]+)', output)
if not match:
raise mesonlib.MesonException(f'Could not find a version number in output of {raw_cmd!r}')
self.cached_version = match.group(1)
return self.cached_version
@classmethod
def from_bin_list(cls, env: 'Environment', for_machine: MachineChoice, name: str) -> 'ExternalProgram':
# There is a static `for_machine` for this class because the binary
# always runs on the build platform. (It's host platform is our build
# platform.) But some external programs have a target platform, so this
# is what we are specifying here.
command = env.lookup_binary_entry(for_machine, name)
if command is None:
return NonExistingExternalProgram()
return cls.from_entry(name, command)
@staticmethod
@functools.lru_cache(maxsize=None)
def _windows_sanitize_path(path: str) -> str:
# Ensure that we use USERPROFILE even when inside MSYS, MSYS2, Cygwin, etc.
if 'USERPROFILE' not in os.environ:
return path
# The WindowsApps directory is a bit of a problem. It contains
# some zero-sized .exe files which have "reparse points", that
# might either launch an installed application, or might open
# a page in the Windows Store to download the application.
#
# To handle the case where the python interpreter we're
# running on came from the Windows Store, if we see the
# WindowsApps path in the search path, replace it with
# dirname(sys.executable).
appstore_dir = Path(os.environ['USERPROFILE']) / 'AppData' / 'Local' / 'Microsoft' / 'WindowsApps'
paths = []
for each in path.split(os.pathsep):
if Path(each) != appstore_dir:
paths.append(each)
elif 'WindowsApps' in sys.executable:
paths.append(os.path.dirname(sys.executable))
return os.pathsep.join(paths)
@staticmethod
def from_entry(name: str, command: T.Union[str, T.List[str]]) -> 'ExternalProgram':
if isinstance(command, list):
if len(command) == 1:
command = command[0]
# We cannot do any searching if the command is a list, and we don't
# need to search if the path is an absolute path.
if isinstance(command, list) or os.path.isabs(command):
if isinstance(command, str):
command = [command]
return ExternalProgram(name, command=command, silent=True)
assert isinstance(command, str)
# Search for the command using the specified string!
return ExternalProgram(command, silent=True)
@staticmethod
def _shebang_to_cmd(script: str) -> T.Optional[T.List[str]]:
"""
Check if the file has a shebang and manually parse it to figure out
the interpreter to use. This is useful if the script is not executable
or if we're on Windows (which does not understand shebangs).
"""
try:
with open(script, encoding='utf-8') as f:
first_line = f.readline().strip()
if first_line.startswith('#!'):
# In a shebang, everything before the first space is assumed to
# be the command to run and everything after the first space is
# the single argument to pass to that command. So we must split
# exactly once.
commands = first_line[2:].split('#')[0].strip().split(maxsplit=1)
if mesonlib.is_windows():
# Windows does not have UNIX paths so remove them,
# but don't remove Windows paths
if commands[0].startswith('/'):
commands[0] = commands[0].split('/')[-1]
if len(commands) > 0 and commands[0] == 'env':
commands = commands[1:]
# Windows does not ship python3.exe, but we know the path to it
if len(commands) > 0 and commands[0] == 'python3':
commands = mesonlib.python_command + commands[1:]
elif mesonlib.is_haiku():
# Haiku does not have /usr, but a lot of scripts assume that
# /usr/bin/env always exists. Detect that case and run the
# script with the interpreter after it.
if commands[0] == '/usr/bin/env':
commands = commands[1:]
# We know what python3 is, we're running on it
if len(commands) > 0 and commands[0] == 'python3':
commands = mesonlib.python_command + commands[1:]
else:
# Replace python3 with the actual python3 that we are using
if commands[0] == '/usr/bin/env' and commands[1] == 'python3':
commands = mesonlib.python_command + commands[2:]
elif commands[0].split('/')[-1] == 'python3':
commands = mesonlib.python_command + commands[1:]
return commands + [script]
except Exception as e:
mlog.debug(str(e))
mlog.debug(f'Unusable script {script!r}')
return None
def _is_executable(self, path: str) -> bool:
suffix = os.path.splitext(path)[-1].lower()[1:]
execmask = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
if mesonlib.is_windows():
if suffix in self.windows_exts:
return True
elif os.stat(path).st_mode & execmask:
return not os.path.isdir(path)
return False
def _search_dir(self, name: str, search_dir: T.Optional[str]) -> T.Optional[list]:
if search_dir is None:
return None
trial = os.path.join(search_dir, name)
if os.path.exists(trial):
if self._is_executable(trial):
return [trial]
# Now getting desperate. Maybe it is a script file that is
# a) not chmodded executable, or
# b) we are on windows so they can't be directly executed.
return self._shebang_to_cmd(trial)
else:
if mesonlib.is_windows():
for ext in self.windows_exts:
trial_ext = f'{trial}.{ext}'
if os.path.exists(trial_ext):
return [trial_ext]
return None
def _search_windows_special_cases(self, name: str, command: str) -> T.List[T.Optional[str]]:
'''
Lots of weird Windows quirks:
1. PATH search for @name returns files with extensions from PATHEXT,
but only self.windows_exts are executable without an interpreter.
2. @name might be an absolute path to an executable, but without the
extension. This works inside MinGW so people use it a lot.
3. The script is specified without an extension, in which case we have
to manually search in PATH.
4. More special-casing for the shebang inside the script.
'''
if command:
# On Windows, even if the PATH search returned a full path, we can't be
# sure that it can be run directly if it's not a native executable.
# For instance, interpreted scripts sometimes need to be run explicitly
# with an interpreter if the file association is not done properly.
name_ext = os.path.splitext(command)[1]
if name_ext[1:].lower() in self.windows_exts:
# Good, it can be directly executed
return [command]
# Try to extract the interpreter from the shebang
commands = self._shebang_to_cmd(command)
if commands:
return commands
return [None]
# Maybe the name is an absolute path to a native Windows
# executable, but without the extension. This is technically wrong,
# but many people do it because it works in the MinGW shell.
if os.path.isabs(name):
for ext in self.windows_exts:
command = f'{name}.{ext}'
if os.path.exists(command):
return [command]
# On Windows, interpreted scripts must have an extension otherwise they
# cannot be found by a standard PATH search. So we do a custom search
# where we manually search for a script with a shebang in PATH.
search_dirs = self._windows_sanitize_path(os.environ.get('PATH', '')).split(';')
for search_dir in search_dirs:
commands = self._search_dir(name, search_dir)
if commands:
return commands
return [None]
def _search(self, name: str, search_dir: T.Optional[str]) -> T.List[T.Optional[str]]:
'''
Search in the specified dir for the specified executable by name
and if not found search in PATH
'''
commands = self._search_dir(name, search_dir)
if commands:
return commands
# If there is a directory component, do not look in PATH
if os.path.dirname(name) and not os.path.isabs(name):
return [None]
# Do a standard search in PATH
path = os.environ.get('PATH', None)
if mesonlib.is_windows() and path:
path = self._windows_sanitize_path(path)
command = shutil.which(name, path=path)
if mesonlib.is_windows():
return self._search_windows_special_cases(name, command)
# On UNIX-like platforms, shutil.which() is enough to find
# all executables whether in PATH or with an absolute path
return [command]
def found(self) -> bool:
return self.command[0] is not None
def get_command(self) -> T.List[str]:
return self.command[:]
def get_path(self) -> T.Optional[str]:
return self.path
def get_name(self) -> str:
return self.name
class NonExistingExternalProgram(ExternalProgram): # lgtm [py/missing-call-to-init]
"A program that will never exist"
def __init__(self, name: str = 'nonexistingprogram') -> None:
self.name = name
self.command = [None]
self.path = None
def __repr__(self) -> str:
r = '<{} {!r} -> {!r}>'
return r.format(self.__class__.__name__, self.name, self.command)
def found(self) -> bool:
return False
class OverrideProgram(ExternalProgram):
"""A script overriding a program."""
def find_external_program(env: 'Environment', for_machine: MachineChoice, name: str,
display_name: str, default_names: T.List[str],
allow_default_for_cross: bool = True) -> T.Generator['ExternalProgram', None, None]:
"""Find an external program, checking the cross file plus any default options."""
potential_names = OrderedSet(default_names)
potential_names.add(name)
# Lookup in cross or machine file.
for potential_name in potential_names:
potential_cmd = env.lookup_binary_entry(for_machine, potential_name)
if potential_cmd is not None:
mlog.debug(f'{display_name} binary for {for_machine} specified from cross file, native file, '
f'or env var as {potential_cmd}')
yield ExternalProgram.from_entry(potential_name, potential_cmd)
# We never fallback if the user-specified option is no good, so
# stop returning options.
return
mlog.debug(f'{display_name} binary missing from cross or native file, or env var undefined.')
# Fallback on hard-coded defaults, if a default binary is allowed for use
# with cross targets, or if this is not a cross target
if allow_default_for_cross or not (for_machine is MachineChoice.HOST and env.is_cross_build(for_machine)):
for potential_path in default_names:
mlog.debug(f'Trying a default {display_name} fallback at', potential_path)
yield ExternalProgram(potential_path, silent=True)
else:
mlog.debug('Default target is not allowed for cross use')
|