|
|
|
# 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.
|
|
|
|
|
|
|
|
"""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, 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(map(
|
|
|
|
lambda t:
|
|
|
|
get_target_from_intro_data(ParsedTargetName(t), builddir, intro_data)['type'] == 'run',
|
|
|
|
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)
|
|
|
|
setup_vsenv(b.need_vsenv)
|
|
|
|
|
|
|
|
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)
|
|
|
|
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.')
|
|
|
|
|
|
|
|
p, *_ = mesonlib.Popen_safe(cmd, stdout=sys.stdout.buffer, stderr=sys.stderr.buffer, env=env)
|
|
|
|
|
|
|
|
return p.returncode
|