diff options
Diffstat (limited to 'mesonbuild/mcompile.py')
-rw-r--r-- | mesonbuild/mcompile.py | 361 |
1 files changed, 361 insertions, 0 deletions
diff --git a/mesonbuild/mcompile.py b/mesonbuild/mcompile.py new file mode 100644 index 0000000..2f5cee9 --- /dev/null +++ b/mesonbuild/mcompile.py @@ -0,0 +1,361 @@ +# Copyright 2020 The Meson development team + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +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 . import coredata +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(None, value, allow_dups=True, user_input=True).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 = defaultdict(list) # type: T.Dict[str, T.List[dict]] + for target in schema: + parsed_data[target['name']] += [target] + return parsed_data + +class ParsedTargetName: + full_name = '' + name = '' + type = '' + path = '' + + 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] + + @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', + '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: + raise MesonException(f'Can\'t invoke target `{target.full_name}`: target not found') + + intro_targets = introspect_data[target.name] + found_targets = [] # type: T.List[T.Dict[str, T.Any]] + + resolved_bdir = builddir.resolve() + + if not target.type and not target.path: + found_targets = intro_targets + else: + for intro_target in intro_targets: + if (intro_target['subproject'] or + (target.type and target.type != intro_target['type'].replace(' ', '_')) 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: + p = Path(i['filename'][0]).relative_to(resolved_bdir) + 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'] == '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'] != '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']) # type: str + 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'] == '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 + +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_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') + + cdata = coredata.load(options.wd) + b = build.load(options.wd) + vsenv_active = setup_vsenv(b.need_vsenv) + if vsenv_active: + mlog.log(mlog.green('INFO:'), 'automatically activated MSVC compiler environment') + + cmd = [] # type: T.List[str] + env = None # type: T.Optional[T.Dict[str, str]] + + 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 |