diff options
-rw-r--r-- | docs/markdown/Commands.md | 39 | ||||
-rw-r--r-- | docs/markdown/snippets/add_meson_compile_target.md | 19 | ||||
-rw-r--r-- | mesonbuild/mcompile.py | 209 | ||||
-rwxr-xr-x | run_unittests.py | 68 |
4 files changed, 291 insertions, 44 deletions
diff --git a/docs/markdown/Commands.md b/docs/markdown/Commands.md index 4d3de55..e2a352a 100644 --- a/docs/markdown/Commands.md +++ b/docs/markdown/Commands.md @@ -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)* diff --git a/docs/markdown/snippets/add_meson_compile_target.md b/docs/markdown/snippets/add_meson_compile_target.md new file mode 100644 index 0000000..d75862f --- /dev/null +++ b/docs/markdown/snippets/add_meson_compile_target.md @@ -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`. diff --git a/mesonbuild/mcompile.py b/mesonbuild/mcompile.py index 3799ce3..9fe3a65 100644 --- a/mesonbuild/mcompile.py +++ b/mesonbuild/mcompile.py @@ -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', @@ -115,6 +249,20 @@ def add_arguments(parser: 'argparse.ArgumentParser') -> None: 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', help='Show more verbose output.' @@ -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( diff --git a/run_unittests.py b/run_unittests.py index 1af0d8b..8a12180 100755 --- a/run_unittests.py +++ b/run_unittests.py @@ -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') |