Added ability to specify target in `meson compile`

pull/4382/head
TheQwertiest 5 years ago committed by Jussi Pakkanen
parent 4d0233540f
commit 5696a5abba
  1. 39
      docs/markdown/Commands.md
  2. 19
      docs/markdown/snippets/add_meson_compile_target.md
  3. 209
      mesonbuild/mcompile.py
  4. 68
      run_unittests.py

@ -136,24 +136,30 @@ meson configure builddir -Doption=new_value
*(since 0.54.0)*
```
$ meson compile [-h] [-j JOBS] [-l LOAD_AVERAGE] [--clean] [-C BUILDDIR]
$ meson compile [-h] [--clean] [-C BUILDDIR] [-j JOBS] [-l LOAD_AVERAGE]
[--verbose] [--ninja-args NINJA_ARGS] [--vs-args VS_ARGS]
[TARGET [TARGET ...]]
```
Builds a default or a specified target of a configured meson project.
```
positional arguments:
TARGET Targets to build. Target has the
following format: [PATH_TO_TARGET/]TARGE
T_NAME[:TARGET_TYPE].
optional arguments:
-h, --help show this help message and exit
--clean Clean the build directory.
-C BUILDDIR The directory containing build files to
be built.
-j JOBS, --jobs JOBS The number of worker jobs to run (if
supported). If the value is less than 1
the build program will guess.
-l LOAD_AVERAGE, --load-average LOAD_AVERAGE
The system load average to try to
maintain (if supported)
--clean Clean the build directory.
-C BUILDDIR The directory containing build files to
be built.
maintain (if supported).
--verbose Show more verbose output.
--ninja-args NINJA_ARGS Arguments to pass to `ninja` (applied
only on `ninja` backend).
@ -161,6 +167,19 @@ optional arguments:
only on `vs` backend).
```
`--verbose` argument is available since 0.55.0.
#### Targets
*(since 0.55.0)*
`TARGET` has the following syntax `[PATH/]NAME[:TYPE]`, where:
- `NAME`: name of the target from `meson.build` (e.g. `foo` from `executable('foo', ...)`).
- `PATH`: path to the target relative to the root `meson.build` file. Note: relative path for a target specified in the root `meson.build` is `./`.
- `TYPE`: type of the target. Can be one of the following: 'executable', 'static_library', 'shared_library', 'shared_module', 'custom', 'run', 'jar'.
`PATH` and/or `TYPE` can be ommited if the resulting `TARGET` can be used to uniquely identify the target in `meson.build`.
#### Backend specific arguments
*(since 0.55.0)*
@ -193,6 +212,16 @@ Execute a dry run on ninja backend with additional debug info:
meson compile --ninja-args=-n,-d,explain
```
Build three targets: two targets that have the same `foo` name, but different type, and a `bar` target:
```
meson compile foo:shared_library foo:static_library bar
```
Produce a coverage html report (if available):
```
meson compile coverage-html
```
### dist
*(since 0.52.0)*

@ -0,0 +1,19 @@
## Added ability to specify targets in `meson compile`
It's now possible to specify targets in `meson compile`, which will result in building only the requested targets.
Usage: `meson compile [TARGET [TARGET...]]`
`TARGET` has the following syntax: `[PATH/]NAME[:TYPE]`.
`NAME`: name of the target from `meson.build` (e.g. `foo` from `executable('foo', ...)`).
`PATH`: path to the target relative to the root `meson.build` file. Note: relative path for a target specified in the root `meson.build` is `./`.
`TYPE`: type of the target (e.g. `shared_library`, `executable` and etc)
`PATH` and/or `TYPE` can be ommited if the resulting `TARGET` can be used to uniquely identify the target in `meson.build`.
For example targets from the following code:
```meson
shared_library('foo', ...)
static_library('foo', ...)
executable('bar', ...)
```
can be invoked with `meson compile foo:shared_library foo:static_library bar`.

@ -14,9 +14,11 @@
"""Entrypoint script for backend agnostic compile."""
import argparse
import json
import re
import sys
import typing as T
from collections import defaultdict
from pathlib import Path
from . import mlog
@ -26,10 +28,13 @@ from .mesonlib import MesonException
from mesonbuild.environment import detect_ninja
from mesonbuild.coredata import UserArrayOption
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):
def validate_builddir(builddir: Path) -> None:
if not (builddir / 'meson-private' / 'coredata.dat' ).is_file():
raise MesonException('Current directory is not a meson build directory: `{}`.\n'
'Please specify a valid build dir or change the working directory to it.\n'
@ -42,7 +47,93 @@ def get_backend_from_coredata(builddir: Path) -> str:
"""
return coredata.load(str(builddir)).get_builtin_option('backend')
def get_parsed_args_ninja(options: 'argparse.Namespace', builddir: Path):
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('`{}` is missing! Directory is not configured yet?'.format(path_to_intro.name))
with path_to_intro.open() 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('Can\'t invoke target `{}`: unknown target type: `{}`'.format(target, 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:
# Ammend 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: dict) -> dict:
if target.name not in introspect_data:
raise MesonException('Can\'t invoke target `{}`: target not found'.format(target.full_name))
intro_targets = introspect_data[target.name]
found_targets = []
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('Can\'t invoke target `{}`: target not found'.format(target.full_name))
elif len(found_targets) > 1:
raise MesonException('Can\'t invoke target `{}`: ambigious name. Add target type and/or path: `PATH/NAME:TYPE`'.format(target.full_name))
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.List[str]:
runner = detect_ninja()
if runner is None:
raise MesonException('Cannot find ninja.')
@ -50,57 +141,100 @@ def get_parsed_args_ninja(options: 'argparse.Namespace', builddir: Path):
cmd = [runner, '-C', builddir.as_posix()]
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')
# 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')
if options.clean:
cmd.append('clean')
cmd.append('--verbose')
cmd += options.ninja_args
return cmd
def get_parsed_args_vs(options: 'argparse.Namespace', builddir: Path):
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('[\%\$\@\;\.\(\)\']', '_', intro_target['id'])
rel_path = Path(intro_target['filename'][0]).relative_to(builddir.resolve()).parent
if rel_path != '.':
target_name = str(rel_path / target_name)
return target_name
def get_parsed_args_vs(options: 'argparse.Namespace', builddir: Path) -> T.List[str]:
slns = list(builddir.glob('*.sln'))
assert len(slns) == 1, 'More than one solution in a project?'
sln = slns[0]
cmd = ['msbuild', str(sln.resolve())]
# In msbuild `-m` with no number means "detect cpus", the default is `-m1`
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('-m{}'.format(options.jobs))
cmd.append('-maxCpuCount:{}'.format(options.jobs))
else:
cmd.append('-m')
cmd.append('-maxCpuCount')
if options.load_average:
mlog.warning('Msbuild does not have a load-average switch, ignoring.')
if not options.verbose:
cmd.append('/v:minimal')
if options.clean:
cmd.append('/t:Clean')
cmd.append('-verbosity:minimal')
cmd += options.vs_args
return cmd
def add_arguments(parser: 'argparse.ArgumentParser') -> None:
"""Add compile specific arguments."""
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=int,
help='The system load average to try to maintain (if supported)'
)
'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',
@ -114,6 +248,20 @@ def add_arguments(parser: 'argparse.ArgumentParser') -> None:
default='.',
help='The directory containing build files to be built.'
)
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=int,
help='The system load average to try to maintain (if supported).'
)
parser.add_argument(
'--verbose',
action='store_true',
@ -138,13 +286,14 @@ def run(options: 'argparse.Namespace') -> int:
cmd = [] # type: T.List[str]
if options.targets and options.clean:
raise MesonException('`TARGET` and `--clean` can\'t be used simultaneously')
backend = get_backend_from_coredata(bdir)
if backend == 'ninja':
cmd = get_parsed_args_ninja(options, bdir)
cmd += options.ninja_args
elif backend.startswith('vs'):
cmd = get_parsed_args_vs(options, bdir)
cmd += options.vs_args
else:
# TODO: xcode?
raise MesonException(

@ -4630,33 +4630,83 @@ recommended as it is not supported on some platforms''')
def test_meson_compile(self):
"""Test the meson compile command."""
prog = 'trivialprog'
if is_windows():
prog = '{}.exe'.format(prog)
def get_exe_name(basename: str) -> str:
if is_windows():
return '{}.exe'.format(basename)
else:
return basename
def get_shared_lib_name(basename: str) -> str:
if mesonbuild.environment.detect_msys2_arch():
return 'lib{}.dll'.format(basename)
elif is_windows():
return '{}.dll'.format(basename)
elif is_cygwin():
return 'cyg{}.dll'.format(basename)
elif is_osx():
return 'lib{}.dylib'.format(basename)
else:
return 'lib{}.so'.format(basename)
def get_static_lib_name(basename: str) -> str:
return 'lib{}.a'.format(basename)
# Base case (no targets or additional arguments)
testdir = os.path.join(self.common_test_dir, '1 trivial')
self.init(testdir)
self._run([*self.meson_command, 'compile', '-C', self.builddir])
# If compile worked then we should get a program
self.assertPathExists(os.path.join(self.builddir, prog))
self.assertPathExists(os.path.join(self.builddir, get_exe_name('trivialprog')))
# `--clean`
self._run([*self.meson_command, 'compile', '-C', self.builddir, '--clean'])
self.assertPathDoesNotExist(os.path.join(self.builddir, prog))
self.assertPathDoesNotExist(os.path.join(self.builddir, get_exe_name('trivialprog')))
# Target specified in a project with unique names
testdir = os.path.join(self.common_test_dir, '6 linkshared')
self.init(testdir, extra_args=['--wipe'])
# Multiple targets and target type specified
self._run([*self.meson_command, 'compile', '-C', self.builddir, 'mylib', 'mycpplib:shared_library'])
# Check that we have a shared lib, but not an executable, i.e. check that target actually worked
self.assertPathExists(os.path.join(self.builddir, get_shared_lib_name('mylib')))
self.assertPathDoesNotExist(os.path.join(self.builddir, get_exe_name('prog')))
self.assertPathExists(os.path.join(self.builddir, get_shared_lib_name('mycpplib')))
self.assertPathDoesNotExist(os.path.join(self.builddir, get_exe_name('cppprog')))
# Target specified in a project with non unique names
testdir = os.path.join(self.common_test_dir, '190 same target name')
self.init(testdir, extra_args=['--wipe'])
self._run([*self.meson_command, 'compile', '-C', self.builddir, './foo'])
self.assertPathExists(os.path.join(self.builddir, get_static_lib_name('foo')))
self._run([*self.meson_command, 'compile', '-C', self.builddir, 'sub/foo'])
self.assertPathExists(os.path.join(self.builddir, 'sub', get_static_lib_name('foo')))
# run_target
testdir = os.path.join(self.common_test_dir, '54 run target')
self.init(testdir, extra_args=['--wipe'])
out = self._run([*self.meson_command, 'compile', '-C', self.builddir, 'py3hi'])
self.assertIn('I am Python3.', out)
# `--$BACKEND-args`
testdir = os.path.join(self.common_test_dir, '1 trivial')
if self.backend is Backend.ninja:
self.init(testdir, extra_args=['--wipe'])
# Dry run - should not create a program
self._run([*self.meson_command, 'compile', '-C', self.builddir, '--ninja-args=-n'])
self.assertPathDoesNotExist(os.path.join(self.builddir, prog))
self.assertPathDoesNotExist(os.path.join(self.builddir, get_exe_name('trivialprog')))
elif self.backend is Backend.vs:
self.init(testdir, extra_args=['--wipe'])
self._run([*self.meson_command, 'compile', '-C', self.builddir])
# Explicitly clean the target through msbuild interface
self._run([*self.meson_command, 'compile', '-C', self.builddir, '--vs-args=-t:{}:Clean'.format(re.sub(r'[\%\$\@\;\.\(\)\']', '_', prog))])
self.assertPathDoesNotExist(os.path.join(self.builddir, prog))
self._run([*self.meson_command, 'compile', '-C', self.builddir, '--vs-args=-t:{}:Clean'.format(re.sub(r'[\%\$\@\;\.\(\)\']', '_', get_exe_name('trivialprog')))])
self.assertPathDoesNotExist(os.path.join(self.builddir, get_exe_name('trivialprog')))
def test_spurious_reconfigure_built_dep_file(self):
testdir = os.path.join(self.unit_test_dir, '75 dep files')

Loading…
Cancel
Save