The Meson Build System
http://mesonbuild.com/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
381 lines
14 KiB
381 lines
14 KiB
# SPDX-License-Identifier: Apache-2.0 |
|
# Copyright 2020 The Meson development team |
|
|
|
from __future__ import annotations |
|
|
|
"""Entrypoint script for backend agnostic compile.""" |
|
|
|
import os |
|
import json |
|
import re |
|
import sys |
|
import shutil |
|
import typing as T |
|
from collections import defaultdict |
|
from pathlib import Path |
|
|
|
from . import mlog |
|
from . import mesonlib |
|
from .mesonlib import MesonException, RealPathAction, join_args, setup_vsenv |
|
from mesonbuild.environment import detect_ninja |
|
from mesonbuild.coredata import UserArrayOption |
|
from mesonbuild import build |
|
|
|
if T.TYPE_CHECKING: |
|
import argparse |
|
|
|
def array_arg(value: str) -> T.List[str]: |
|
return UserArrayOption.listify_value(value) |
|
|
|
def validate_builddir(builddir: Path) -> None: |
|
if not (builddir / 'meson-private' / 'coredata.dat').is_file(): |
|
raise MesonException(f'Current directory is not a meson build directory: `{builddir}`.\n' |
|
'Please specify a valid build dir or change the working directory to it.\n' |
|
'It is also possible that the build directory was generated with an old\n' |
|
'meson version. Please regenerate it in this case.') |
|
|
|
def parse_introspect_data(builddir: Path) -> T.Dict[str, T.List[dict]]: |
|
""" |
|
Converts a List of name-to-dict to a dict of name-to-dicts (since names are not unique) |
|
""" |
|
path_to_intro = builddir / 'meson-info' / 'intro-targets.json' |
|
if not path_to_intro.exists(): |
|
raise MesonException(f'`{path_to_intro.name}` is missing! Directory is not configured yet?') |
|
with path_to_intro.open(encoding='utf-8') as f: |
|
schema = json.load(f) |
|
|
|
parsed_data: T.Dict[str, T.List[dict]] = defaultdict(list) |
|
for target in schema: |
|
parsed_data[target['name']] += [target] |
|
return parsed_data |
|
|
|
class ParsedTargetName: |
|
full_name = '' |
|
base_name = '' |
|
name = '' |
|
type = '' |
|
path = '' |
|
suffix = '' |
|
|
|
def __init__(self, target: str): |
|
self.full_name = target |
|
split = target.rsplit(':', 1) |
|
if len(split) > 1: |
|
self.type = split[1] |
|
if not self._is_valid_type(self.type): |
|
raise MesonException(f'Can\'t invoke target `{target}`: unknown target type: `{self.type}`') |
|
|
|
split = split[0].rsplit('/', 1) |
|
if len(split) > 1: |
|
self.path = split[0] |
|
self.name = split[1] |
|
else: |
|
self.name = split[0] |
|
|
|
split = self.name.rsplit('.', 1) |
|
if len(split) > 1: |
|
self.base_name = split[0] |
|
self.suffix = split[1] |
|
else: |
|
self.base_name = split[0] |
|
|
|
@staticmethod |
|
def _is_valid_type(type: str) -> bool: |
|
# Amend docs in Commands.md when editing this list |
|
allowed_types = { |
|
'executable', |
|
'static_library', |
|
'shared_library', |
|
'shared_module', |
|
'custom', |
|
'alias', |
|
'run', |
|
'jar', |
|
} |
|
return type in allowed_types |
|
|
|
def get_target_from_intro_data(target: ParsedTargetName, builddir: Path, introspect_data: T.Dict[str, T.Any]) -> T.Dict[str, T.Any]: |
|
if target.name not in introspect_data and target.base_name not in introspect_data: |
|
raise MesonException(f'Can\'t invoke target `{target.full_name}`: target not found') |
|
|
|
intro_targets = introspect_data[target.name] |
|
# if target.name doesn't find anything, try just the base name |
|
if not intro_targets: |
|
intro_targets = introspect_data[target.base_name] |
|
found_targets: T.List[T.Dict[str, T.Any]] = [] |
|
|
|
resolved_bdir = builddir.resolve() |
|
|
|
if not target.type and not target.path and not target.suffix: |
|
found_targets = intro_targets |
|
else: |
|
for intro_target in intro_targets: |
|
# Parse out the name from the id if needed |
|
intro_target_name = intro_target['name'] |
|
split = intro_target['id'].rsplit('@', 1) |
|
if len(split) > 1: |
|
split = split[0].split('@@', 1) |
|
if len(split) > 1: |
|
intro_target_name = split[1] |
|
else: |
|
intro_target_name = split[0] |
|
if ((target.type and target.type != intro_target['type'].replace(' ', '_')) or |
|
(target.name != intro_target_name) or |
|
(target.path and intro_target['filename'] != 'no_name' and |
|
Path(target.path) != Path(intro_target['filename'][0]).relative_to(resolved_bdir).parent)): |
|
continue |
|
found_targets += [intro_target] |
|
|
|
if not found_targets: |
|
raise MesonException(f'Can\'t invoke target `{target.full_name}`: target not found') |
|
elif len(found_targets) > 1: |
|
suggestions: T.List[str] = [] |
|
for i in found_targets: |
|
i_name = i['name'] |
|
split = i['id'].rsplit('@', 1) |
|
if len(split) > 1: |
|
split = split[0].split('@@', 1) |
|
if len(split) > 1: |
|
i_name = split[1] |
|
else: |
|
i_name = split[0] |
|
p = Path(i['filename'][0]).relative_to(resolved_bdir).parent / i_name |
|
t = i['type'].replace(' ', '_') |
|
suggestions.append(f'- ./{p}:{t}') |
|
suggestions_str = '\n'.join(suggestions) |
|
raise MesonException(f'Can\'t invoke target `{target.full_name}`: ambiguous name.' |
|
f' Add target type and/or path:\n{suggestions_str}') |
|
|
|
return found_targets[0] |
|
|
|
def generate_target_names_ninja(target: ParsedTargetName, builddir: Path, introspect_data: dict) -> T.List[str]: |
|
intro_target = get_target_from_intro_data(target, builddir, introspect_data) |
|
|
|
if intro_target['type'] in {'alias', 'run'}: |
|
return [target.name] |
|
else: |
|
return [str(Path(out_file).relative_to(builddir.resolve())) for out_file in intro_target['filename']] |
|
|
|
def get_parsed_args_ninja(options: 'argparse.Namespace', builddir: Path) -> T.Tuple[T.List[str], T.Optional[T.Dict[str, str]]]: |
|
runner = detect_ninja() |
|
if runner is None: |
|
raise MesonException('Cannot find ninja.') |
|
|
|
cmd = runner |
|
if not builddir.samefile('.'): |
|
cmd.extend(['-C', builddir.as_posix()]) |
|
|
|
# If the value is set to < 1 then don't set anything, which let's |
|
# ninja/samu decide what to do. |
|
if options.jobs > 0: |
|
cmd.extend(['-j', str(options.jobs)]) |
|
if options.load_average > 0: |
|
cmd.extend(['-l', str(options.load_average)]) |
|
|
|
if options.verbose: |
|
cmd.append('-v') |
|
|
|
cmd += options.ninja_args |
|
|
|
# operands must be processed after options/option-arguments |
|
if options.targets: |
|
intro_data = parse_introspect_data(builddir) |
|
for t in options.targets: |
|
cmd.extend(generate_target_names_ninja(ParsedTargetName(t), builddir, intro_data)) |
|
if options.clean: |
|
cmd.append('clean') |
|
|
|
return cmd, None |
|
|
|
def generate_target_name_vs(target: ParsedTargetName, builddir: Path, introspect_data: dict) -> str: |
|
intro_target = get_target_from_intro_data(target, builddir, introspect_data) |
|
|
|
assert intro_target['type'] not in {'alias', 'run'}, 'Should not reach here: `run` targets must be handle above' |
|
|
|
# Normalize project name |
|
# Source: https://docs.microsoft.com/en-us/visualstudio/msbuild/how-to-build-specific-targets-in-solutions-by-using-msbuild-exe |
|
target_name = re.sub(r"[\%\$\@\;\.\(\)']", '_', intro_target['id']) |
|
rel_path = Path(intro_target['filename'][0]).relative_to(builddir.resolve()).parent |
|
if rel_path != Path('.'): |
|
target_name = str(rel_path / target_name) |
|
return target_name |
|
|
|
def get_parsed_args_vs(options: 'argparse.Namespace', builddir: Path) -> T.Tuple[T.List[str], T.Optional[T.Dict[str, str]]]: |
|
slns = list(builddir.glob('*.sln')) |
|
assert len(slns) == 1, 'More than one solution in a project?' |
|
sln = slns[0] |
|
|
|
cmd = ['msbuild'] |
|
|
|
if options.targets: |
|
intro_data = parse_introspect_data(builddir) |
|
has_run_target = any( |
|
get_target_from_intro_data(ParsedTargetName(t), builddir, intro_data)['type'] in {'alias', 'run'} |
|
for t in options.targets) |
|
|
|
if has_run_target: |
|
# `run` target can't be used the same way as other targets on `vs` backend. |
|
# They are defined as disabled projects, which can't be invoked as `.sln` |
|
# target and have to be invoked directly as project instead. |
|
# Issue: https://github.com/microsoft/msbuild/issues/4772 |
|
|
|
if len(options.targets) > 1: |
|
raise MesonException('Only one target may be specified when `run` target type is used on this backend.') |
|
intro_target = get_target_from_intro_data(ParsedTargetName(options.targets[0]), builddir, intro_data) |
|
proj_dir = Path(intro_target['filename'][0]).parent |
|
proj = proj_dir/'{}.vcxproj'.format(intro_target['id']) |
|
cmd += [str(proj.resolve())] |
|
else: |
|
cmd += [str(sln.resolve())] |
|
cmd.extend(['-target:{}'.format(generate_target_name_vs(ParsedTargetName(t), builddir, intro_data)) for t in options.targets]) |
|
else: |
|
cmd += [str(sln.resolve())] |
|
|
|
if options.clean: |
|
cmd.extend(['-target:Clean']) |
|
|
|
# In msbuild `-maxCpuCount` with no number means "detect cpus", the default is `-maxCpuCount:1` |
|
if options.jobs > 0: |
|
cmd.append(f'-maxCpuCount:{options.jobs}') |
|
else: |
|
cmd.append('-maxCpuCount') |
|
|
|
if options.load_average: |
|
mlog.warning('Msbuild does not have a load-average switch, ignoring.') |
|
|
|
if not options.verbose: |
|
cmd.append('-verbosity:minimal') |
|
|
|
cmd += options.vs_args |
|
|
|
# Remove platform from env if set so that msbuild does not |
|
# pick x86 platform when solution platform is Win32 |
|
env = os.environ.copy() |
|
env.pop('PLATFORM', None) |
|
|
|
return cmd, env |
|
|
|
def get_parsed_args_xcode(options: 'argparse.Namespace', builddir: Path) -> T.Tuple[T.List[str], T.Optional[T.Dict[str, str]]]: |
|
runner = 'xcodebuild' |
|
if not shutil.which(runner): |
|
raise MesonException('Cannot find xcodebuild, did you install XCode?') |
|
|
|
# No argument to switch directory |
|
os.chdir(str(builddir)) |
|
|
|
cmd = [runner, '-parallelizeTargets'] |
|
|
|
if options.targets: |
|
for t in options.targets: |
|
cmd += ['-target', t] |
|
|
|
if options.clean: |
|
if options.targets: |
|
cmd += ['clean'] |
|
else: |
|
cmd += ['-alltargets', 'clean'] |
|
# Otherwise xcodebuild tries to delete the builddir and fails |
|
cmd += ['-UseNewBuildSystem=FALSE'] |
|
|
|
if options.jobs > 0: |
|
cmd.extend(['-jobs', str(options.jobs)]) |
|
|
|
if options.load_average > 0: |
|
mlog.warning('xcodebuild does not have a load-average switch, ignoring') |
|
|
|
if options.verbose: |
|
# xcodebuild is already quite verbose, and -quiet doesn't print any |
|
# status messages |
|
pass |
|
|
|
cmd += options.xcode_args |
|
return cmd, None |
|
|
|
# Note: when adding arguments, please also add them to the completion |
|
# scripts in $MESONSRC/data/shell-completions/ |
|
def add_arguments(parser: 'argparse.ArgumentParser') -> None: |
|
"""Add compile specific arguments.""" |
|
parser.add_argument( |
|
'targets', |
|
metavar='TARGET', |
|
nargs='*', |
|
default=None, |
|
help='Targets to build. Target has the following format: [PATH_TO_TARGET/]TARGET_NAME.TARGET_SUFFIX[:TARGET_TYPE].') |
|
parser.add_argument( |
|
'--clean', |
|
action='store_true', |
|
help='Clean the build directory.' |
|
) |
|
parser.add_argument('-C', dest='wd', action=RealPathAction, |
|
help='directory to cd into before running') |
|
|
|
parser.add_argument( |
|
'-j', '--jobs', |
|
action='store', |
|
default=0, |
|
type=int, |
|
help='The number of worker jobs to run (if supported). If the value is less than 1 the build program will guess.' |
|
) |
|
parser.add_argument( |
|
'-l', '--load-average', |
|
action='store', |
|
default=0, |
|
type=float, |
|
help='The system load average to try to maintain (if supported).' |
|
) |
|
parser.add_argument( |
|
'-v', '--verbose', |
|
action='store_true', |
|
help='Show more verbose output.' |
|
) |
|
parser.add_argument( |
|
'--ninja-args', |
|
type=array_arg, |
|
default=[], |
|
help='Arguments to pass to `ninja` (applied only on `ninja` backend).' |
|
) |
|
parser.add_argument( |
|
'--vs-args', |
|
type=array_arg, |
|
default=[], |
|
help='Arguments to pass to `msbuild` (applied only on `vs` backend).' |
|
) |
|
parser.add_argument( |
|
'--xcode-args', |
|
type=array_arg, |
|
default=[], |
|
help='Arguments to pass to `xcodebuild` (applied only on `xcode` backend).' |
|
) |
|
|
|
def run(options: 'argparse.Namespace') -> int: |
|
bdir = Path(options.wd) |
|
validate_builddir(bdir) |
|
if options.targets and options.clean: |
|
raise MesonException('`TARGET` and `--clean` can\'t be used simultaneously') |
|
|
|
b = build.load(options.wd) |
|
cdata = b.environment.coredata |
|
need_vsenv = T.cast('bool', cdata.get_option(mesonlib.OptionKey('vsenv'))) |
|
if setup_vsenv(need_vsenv): |
|
mlog.log(mlog.green('INFO:'), 'automatically activated MSVC compiler environment') |
|
|
|
cmd: T.List[str] = [] |
|
env: T.Optional[T.Dict[str, str]] = None |
|
|
|
backend = cdata.get_option(mesonlib.OptionKey('backend')) |
|
assert isinstance(backend, str) |
|
mlog.log(mlog.green('INFO:'), 'autodetecting backend as', backend) |
|
if backend == 'ninja': |
|
cmd, env = get_parsed_args_ninja(options, bdir) |
|
elif backend.startswith('vs'): |
|
cmd, env = get_parsed_args_vs(options, bdir) |
|
elif backend == 'xcode': |
|
cmd, env = get_parsed_args_xcode(options, bdir) |
|
else: |
|
raise MesonException( |
|
f'Backend `{backend}` is not yet supported by `compile`. Use generated project files directly instead.') |
|
|
|
mlog.log(mlog.green('INFO:'), 'calculating backend command to run:', join_args(cmd)) |
|
p, *_ = mesonlib.Popen_safe(cmd, stdout=sys.stdout.buffer, stderr=sys.stderr.buffer, env=env) |
|
|
|
return p.returncode
|
|
|