summaryrefslogtreecommitdiffstats
path: root/mesonbuild/mcompile.py
diff options
context:
space:
mode:
Diffstat (limited to 'mesonbuild/mcompile.py')
-rw-r--r--mesonbuild/mcompile.py361
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