diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py index 1057892c668a..dbaf6359831b 100644 --- a/mesonbuild/backend/ninjabackend.py +++ b/mesonbuild/backend/ninjabackend.py @@ -491,7 +491,7 @@ def generate_custom_target(self, target, outfile): (srcs, ofilenames, cmd) = self.eval_custom_target_command(target) deps = self.unwrap_dep_list(target) deps += self.get_custom_target_depend_files(target) - desc = 'Generating {0} with a {1} command.' + desc = 'Generating custom target {!r} with {}' if target.build_always: deps.append('PHONY') if target.depfile is None: @@ -506,11 +506,22 @@ def generate_custom_target(self, target, outfile): elem.add_dep(os.path.join(self.get_target_dir(d), output)) serialize = False extra_paths = [] + exe = target.command[0] + # Get the command's basename for printing when the target is run + if isinstance(exe, (dependencies.ExternalProgram, + build.BuildTarget, build.CustomTarget)): + basename = exe.name + else: + basename = os.path.basename(exe) # If the target requires capturing stdout, then use the serialized # executable wrapper to capture that output and save it to a file. - if target.capture: + if target.capture or target.envvars: + serialize = True + # If the target requires environment variables set, we need the wrapper + # to set those vars for us. + if target.envvars: serialize = True - # If the command line requires a newline, also use the wrapper, as + # If the command line requires a newline, also use the wrapper as # ninja does not support them in its build rule syntax. if any('\n' in c for c in cmd): serialize = True @@ -520,27 +531,30 @@ def generate_custom_target(self, target, outfile): # CustomTarget command needs extra paths first. if mesonlib.is_windows() or mesonlib.is_cygwin(): extra_bdeps = target.get_transitive_build_target_deps() - extra_paths = self.determine_windows_extra_paths(target.command[0], extra_bdeps) + extra_paths = self.determine_windows_extra_paths(exe, extra_bdeps) if extra_paths: serialize = True if serialize: - exe_data = self.serialize_executable(target.command[0], cmd[1:], + exe_data = self.serialize_executable(exe, cmd[1:], # All targets are built from the build dir self.environment.get_build_dir(), extra_paths=extra_paths, + env=target.envvars, capture=ofilenames[0] if target.capture else None) + cmd_str = '{!r} using the meson_exe.py wrapper'.format(basename) cmd = self.environment.get_build_command() + ['--internal', 'exe', exe_data] - cmd_type = 'meson_exe.py custom' else: - cmd_type = 'custom' + cmd_str = '{!r}'.format(basename) if target.depfile is not None: rel_dfile = os.path.join(self.get_target_dir(target), target.depfile) abs_pdir = os.path.join(self.environment.get_build_dir(), self.get_target_dir(target)) os.makedirs(abs_pdir, exist_ok=True) elem.add_item('DEPFILE', rel_dfile) + if target.ninja_pool is not None: + elem.add_item('pool', target.ninja_pool) cmd = self.replace_paths(target, cmd) elem.add_item('COMMAND', cmd) - elem.add_item('description', desc.format(target.name, cmd_type)) + elem.add_item('description', desc.format(target.name, cmd_str)) elem.write(outfile) self.processed_targets[target.get_id()] = True diff --git a/mesonbuild/backend/vs2010backend.py b/mesonbuild/backend/vs2010backend.py index e872a0474c5c..6d9de57abedf 100644 --- a/mesonbuild/backend/vs2010backend.py +++ b/mesonbuild/backend/vs2010backend.py @@ -434,6 +434,7 @@ def gen_custom_target_vcxproj(self, target, ofname, guid): exe_data = self.serialize_executable(target.command[0], cmd[1:], # All targets run from the target dir tdir_abs, + env=target.envvars, extra_paths=extra_paths, capture=ofilenames[0] if target.capture else None) wrapper_cmd = self.environment.get_build_command() + ['--internal', 'exe', exe_data] diff --git a/mesonbuild/build.py b/mesonbuild/build.py index 7757300c5e03..65d7f3dd26c3 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -13,6 +13,7 @@ # limitations under the License. import copy, os, re +from pathlib import PurePath from collections import OrderedDict import itertools @@ -1517,6 +1518,8 @@ def __init__(self, name, subdir, subproject, kwargs, absolute_paths=False): self.extra_depends = [] self.depend_files = [] # Files that this target depends on but are not on the command line. self.depfile = None + self.envvars = {} + self.ninja_pool = None self.process_kwargs(kwargs) self.extra_files = [] # Whether to use absolute paths for all files on the commandline @@ -1601,10 +1604,13 @@ def process_kwargs(self, kwargs): inputs = get_sources_string_names(self.sources) values = get_filenames_templates_dict(inputs, []) for i in self.outputs: - if not(isinstance(i, str)): + if not isinstance(i, str): raise InvalidArguments('Output argument not a string.') - if '/' in i: - raise InvalidArguments('Output must not contain a path segment.') + ipath = PurePath(i) + if ipath.is_absolute(): + raise InvalidArguments('Output must not be an absolute path') + if '..' in ipath.parts: + raise InvalidArguments('Output path must not contain ".."') if '@INPUT@' in i or '@INPUT0@' in i: m = 'Output cannot contain @INPUT@ or @INPUT0@, did you ' \ 'mean @PLAINNAME@ or @BASENAME@?' @@ -1625,8 +1631,11 @@ def process_kwargs(self, kwargs): depfile = kwargs['depfile'] if not isinstance(depfile, str): raise InvalidArguments('Depfile must be a string.') - if os.path.split(depfile)[1] != depfile: - raise InvalidArguments('Depfile must be a plain filename without a subdirectory.') + deppath = PurePath(depfile) + if deppath.is_absolute(): + raise InvalidArguments('Depfile must not be an absolute path') + if '..' in deppath.parts: + raise InvalidArguments('Depfile must not contain ".."') self.depfile = depfile self.command = self.flatten_command(kwargs['command']) if self.capture: diff --git a/mesonbuild/mesonlib.py b/mesonbuild/mesonlib.py index 9ad0668214d4..cb8ad873471d 100644 --- a/mesonbuild/mesonlib.py +++ b/mesonbuild/mesonlib.py @@ -661,6 +661,8 @@ def expand_arguments(args): return None return expended_args +Popen_safe_errors = (FileNotFoundError, PermissionError, subprocess.CalledProcessError) + def Popen_safe(args, write=None, stderr=subprocess.PIPE, **kwargs): if sys.version_info < (3, 6) or not sys.stdout.encoding: return Popen_safe_legacy(args, write=write, stderr=stderr, **kwargs) diff --git a/mesonbuild/modules/unstable_cargo.py b/mesonbuild/modules/unstable_cargo.py new file mode 100644 index 000000000000..096efcb7bf9e --- /dev/null +++ b/mesonbuild/modules/unstable_cargo.py @@ -0,0 +1,307 @@ +# Copyright 2017 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. + +import os +import json + +from .. import mlog +from ..mesonlib import Popen_safe, Popen_safe_errors +from ..mesonlib import extract_as_list, version_compare, MesonException, File +from ..environment import for_windows, for_cygwin, for_darwin +from ..interpreterbase import noKwargs, permittedKwargs +from ..dependencies import InternalDependency +from ..build import CustomTarget + +from . import ExtensionModule, ModuleReturnValue, permittedSnippetKwargs + +target_kwargs = {'toml', 'sources', 'install', 'install_dir'} + + +class CargoModule(ExtensionModule): + cargo = ['cargo'] + cargo_build = ['cargo', 'build', '-v', '--color=always'] + cargo_version = None + + def __init__(self): + super().__init__() + try: + self._get_version() + except Popen_safe_errors: + raise MesonException('Cargo was not found') + self.snippets.add('test') + self.snippets.add('benchmark') + + def _get_cargo_build(self, toml): + # FIXME: must use target-triple to set the host system, currently it + # always builds for the build machine. + return self.cargo_build + ['--manifest-path', toml] + + def _is_release(self, env): + buildtype = env.coredata.get_builtin_option('buildtype') + return not buildtype.startswith('debug') + + def _get_crate_name(self, name, crate_type, env): + ''' + We have no control over what filenames cargo uses for its output, so we + have to figure it out ourselves. + ''' + if for_cygwin(env.is_cross_build(), env): + raise MesonException('Cygwin cargo support is TODO') + prefix = 'lib' + if crate_type == 'staticlib': + suffix = 'a' + if for_windows(env.is_cross_build(), env): + prefix = '' + suffix = 'lib' + elif crate_type == 'cdylib': + if for_windows(env.is_cross_build(), env): + prefix = '' + suffix = 'dll' + elif for_darwin(env.is_cross_build(), env): + suffix = 'dylib' + else: + suffix = 'so' + elif crate_type == 'bin': + prefix = '' + suffix = '' + if for_windows(env.is_cross_build(), env): + suffix = 'exe' + if suffix: + fname = prefix + name + '.' + suffix + else: + fname = prefix + name + if self._is_release(env): + return os.path.join('release', fname) + else: + return os.path.join('debug', fname) + + def _read_metadata(self, toml): + cmd = self.cargo + ['metadata', '--format-version=1', '--no-deps', + '--manifest-path', toml] + out = Popen_safe(cmd)[1] + try: + encoded = json.loads(out) + except json.decoder.JSONDecodeError: + print(cmd, out) + raise + return encoded['packages'][0] + + def _source_strings_to_files(self, source_dir, subdir, sources): + results = [] + for s in sources: + if isinstance(s, File): + pass + elif isinstance(s, str): + s = File.from_source_file(source_dir, subdir, s) + else: + raise MesonException('Source item is {!r} instead of ' + 'string or files() object'.format(s)) + results.append(s) + return results + + def _get_sources(self, state, kwargs): + # 'sources' kwargs is optional; we have a depfile with dependency + # information and ninja will use that to determine when to rebuild. + sources = extract_as_list(kwargs, 'sources') + return self._source_strings_to_files(state.environment.source_dir, + state.subdir, sources) + + def _get_cargo_test_outputs(self, name, metadata, env): + args = [] + outputs = [] + depfile = None + for t in metadata['targets']: + if t['name'] != name: + continue + # Filter out crate types we don't want + # a test target will only have one output + if t['crate_types'] != ['bin']: + continue + # Filter out the target `kind`s that we don't want + if t['kind'] != ['test']: + continue + outputs.append(self._get_crate_name(name, 'bin', env)) + args = ['--test', name] + break + if outputs: + depfile = os.path.splitext(outputs[0])[0] + '.d' + else: + toml = metadata['manifest_path'] + raise MesonException('no test called {!r} found in {!r}' + ''.format(name, toml)) + return outputs, depfile, args + + def _get_cargo_executable_outputs(self, name, metadata, env): + args = [] + outputs = [] + depfile = None + for t in metadata['targets']: + if t['name'] != name: + continue + # Filter out crate types we don't want + # an executable target will only have one output + if t['crate_types'] != ['bin']: + continue + # Filter out the target `kind`s that we don't want + if t['kind'] not in [['example'], ['bin']]: + continue + outputs.append(self._get_crate_name(name, 'bin', env)) + if t['kind'][0] == 'example': + args = ['--example', name] + else: + args = ['--bin', name] + break + if outputs: + depfile = os.path.splitext(outputs[0])[0] + '.d' + else: + toml = metadata['manifest_path'] + raise MesonException('no bin called {!r} found in {!r}' + ''.format(name, toml)) + return outputs, depfile, args + + def _get_cargo_static_library_outputs(self, name, metadata, env): + args = [] + outputs = [] + depfile = None + for t in metadata['targets']: + if t['name'] != name: + continue + # Filter out the target `kind`s that we don't want + # a library target can have multiple outputs + if 'staticlib' not in t['kind'] and \ + 'example' not in t['kind']: + continue + for ct in t['crate_types']: + if ct == 'staticlib': + outputs.append(self._get_crate_name(name, ct, env)) + if t['kind'][0] == 'example': + # If the library is an example, it must be built by name + args = ['--example', name] + else: + # Library is the crate itself, no name needed + args = ['--lib'] + break + if outputs: + depfile = os.path.splitext(outputs[0])[0] + '.d' + else: + toml = metadata['manifest_path'] + raise MesonException('no staticlib called {!r} found ' + 'in {!r}'.format(name, toml)) + return outputs, depfile, args + + def _get_cargo_outputs(self, name, metadata, env, cargo_target_type): + # FIXME: track which outputs have already been fetched from + # a toml file and disallow duplicates. + fn = getattr(self, '_get_cargo_{}_outputs'.format(cargo_target_type)) + return fn(name, metadata, env) + + def _check_cargo_dep_info_bug(self, metadata): + if version_compare(self.cargo_version, '>0.22.0'): + return + for t in metadata['targets']: + if t['kind'] == ['custom-build']: + m = 'Crate {!r} contains a custom build script {!r} which ' \ + 'will cause dep-info to not being emitted due to a ' \ + 'bug in Cargo. Please upgrade to Cargo 0.23 or newer.' \ + ''.format(metadata['name'], os.path.basename(t['src_path'])) + mlog.warning(m) + return + + def _cargo_target(self, state, args, kwargs, cargo_target_type): + ctkwargs = {} + env = state.environment + if len(args) != 1: + raise MesonException('{0}() requires exactly one positional ' + 'argument: the name of the {0}' + ''.format(cargo_target_type)) + name = args[0] + if 'toml' not in kwargs: + raise MesonException('"toml" kwarg is required') + toml = File.from_source_file(env.get_source_dir(), state.subdir, + kwargs['toml']) + # Get the Cargo.toml file as a JSON encoded object + md = self._read_metadata(toml.absolute_path(env.source_dir, None)) + # Warn about the cargo dep-info bug if needed + self._check_cargo_dep_info_bug(md) + # Get the list of outputs that cargo will create matching the specified name + ctkwargs['output'], ctkwargs['depfile'], cargo_args = \ + self._get_cargo_outputs(name, md, env, cargo_target_type) + # Set the files that will trigger a rebuild + ctkwargs['depend_files'] = [toml] + self._get_sources(state, kwargs) + # Cargo command that will build the output library/libraries/bins + cmd = self._get_cargo_build(toml) + cargo_args + if self._is_release(env): + cmd.append('--release') + ctkwargs['command'] = cmd + if 'install' in kwargs: + ctkwargs['install'] = kwargs['install'] + if 'install_dir' in kwargs: + ctkwargs['install_dir'] = kwargs['install_dir'] + elif 'install' in kwargs: + # People should be able to set `install: true` and get a good + # default for `install_dir` + if cargo_target_type == 'static_library': + ctkwargs['install_dir'] = env.coredata.get_builtin_option('libdir') + elif cargo_target_type == 'executable': + ctkwargs['install_dir'] = env.coredata.get_builtin_option('bindir') + ct = CustomTarget(name, state.subdir, state.subproject, ctkwargs) + # Ninja buffers all cargo output so we get no status updates + ct.ninja_pool = 'console' + # Force it to output in the current directory + ct.envvars['CARGO_TARGET_DIR'] = state.subdir + # XXX: we need to call `cargo clean` on `ninja clean`. + return md, ct + + @permittedKwargs(target_kwargs) + def static_library(self, state, args, kwargs): + md, ct = self._cargo_target(state, args, kwargs, 'static_library') + # XXX: Cargo build outputs a list of system libraries that are needed + # by this (possibly) static library, but we have no way of accessing it + # during configure. So we require developers to manage that themselves. + # XXX: We add the output file into `sources`, but that creates + # a compile-time dependency instead of a link-time dependency and + # reduces parallelism. + d = InternalDependency(md['version'], [], [], [], [], [ct], []) + return ModuleReturnValue(d, [d]) + + @permittedKwargs(target_kwargs) + def executable(self, state, args, kwargs): + md, ct = self._cargo_target(state, args, kwargs, 'executable') + # XXX: We return a custom target, which means this may not be usable + # everywhere that an executable build target can be. + return ModuleReturnValue(ct, [ct]) + + @permittedSnippetKwargs(target_kwargs) + def test(self, interpreter, state, args, kwargs): + # This would map to cargo tests + raise MesonException('Not implemented') + + @permittedSnippetKwargs(target_kwargs) + def benchmark(self, interpreter, state, args, kwargs): + # This would map to cargo benches + raise MesonException('Not implemented') + + def _get_version(self): + if self.cargo_version is None: + out = Popen_safe(self.cargo + ['--version'])[1] + self.cargo_version = out.strip().split('cargo ')[1] + return self.cargo_version + + @noKwargs + def version(self, state, args, kwargs): + return ModuleReturnValue(self._get_version(), []) + + +def initialize(): + return CargoModule() diff --git a/mesonbuild/scripts/meson_exe.py b/mesonbuild/scripts/meson_exe.py index 643e1af7b6d3..550d69236e20 100644 --- a/mesonbuild/scripts/meson_exe.py +++ b/mesonbuild/scripts/meson_exe.py @@ -17,6 +17,7 @@ import argparse import pickle import platform +import subprocess from ..mesonlib import Popen_safe @@ -56,7 +57,12 @@ def run_exe(exe): if len(exe.extra_paths) > 0: child_env['PATH'] = (os.pathsep.join(exe.extra_paths + ['']) + child_env['PATH']) - p, stdout, stderr = Popen_safe(cmd + exe.cmd_args, env=child_env, cwd=exe.workdir) + # By default, don't capture output + stderr = None + if exe.capture: + stderr = subprocess.PIPE + p, stdout, stderr = Popen_safe(cmd + exe.cmd_args, env=child_env, + cwd=exe.workdir, stderr=stderr) if exe.capture and p.returncode == 0: with open(exe.capture, 'w') as output: output.write(stdout) diff --git a/test cases/rust/7 cargo module/.gitignore b/test cases/rust/7 cargo module/.gitignore new file mode 100644 index 000000000000..03314f77b5aa --- /dev/null +++ b/test cases/rust/7 cargo module/.gitignore @@ -0,0 +1 @@ +Cargo.lock diff --git a/test cases/rust/7 cargo module/bins/Cargo.toml b/test cases/rust/7 cargo module/bins/Cargo.toml new file mode 100644 index 000000000000..f3745cdbc797 --- /dev/null +++ b/test cases/rust/7 cargo module/bins/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "mesonbin" +version = "0.1.0" +authors = ["Nirbheek Chauhan "] + +[dependencies] diff --git a/test cases/rust/7 cargo module/bins/meson.build b/test cases/rust/7 cargo module/bins/meson.build new file mode 100644 index 000000000000..b2b9efcdd385 --- /dev/null +++ b/test cases/rust/7 cargo module/bins/meson.build @@ -0,0 +1,5 @@ +cargo.executable('mesonbin', + toml : 'Cargo.toml', + # This is optional, since ninja keeps track of dependencies on source files. + sources : ['src/main.rs'], + install : true) diff --git a/test cases/rust/7 cargo module/bins/src/main.rs b/test cases/rust/7 cargo module/bins/src/main.rs new file mode 100644 index 000000000000..e7a11a969c03 --- /dev/null +++ b/test cases/rust/7 cargo module/bins/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/test cases/rust/7 cargo module/bothlibs/Cargo.toml b/test cases/rust/7 cargo module/bothlibs/Cargo.toml new file mode 100644 index 000000000000..d35932e2245d --- /dev/null +++ b/test cases/rust/7 cargo module/bothlibs/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "mesonbothlib" +version = "0.1.0" +authors = ["Nirbheek Chauhan "] + +[dependencies] + +[lib] +crate-type = ['staticlib', 'cdylib'] diff --git a/test cases/rust/7 cargo module/bothlibs/meson.build b/test cases/rust/7 cargo module/bothlibs/meson.build new file mode 100644 index 000000000000..4627a3d5628f --- /dev/null +++ b/test cases/rust/7 cargo module/bothlibs/meson.build @@ -0,0 +1,3 @@ +cargo.static_library('mesonbothlib', + toml : 'Cargo.toml', + install : true) diff --git a/test cases/rust/7 cargo module/bothlibs/src/lib.rs b/test cases/rust/7 cargo module/bothlibs/src/lib.rs new file mode 100644 index 000000000000..31e1bb209f98 --- /dev/null +++ b/test cases/rust/7 cargo module/bothlibs/src/lib.rs @@ -0,0 +1,7 @@ +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + assert_eq!(2 + 2, 4); + } +} diff --git a/test cases/rust/7 cargo module/installed_files.txt b/test cases/rust/7 cargo module/installed_files.txt new file mode 100644 index 000000000000..117efb430283 --- /dev/null +++ b/test cases/rust/7 cargo module/installed_files.txt @@ -0,0 +1,5 @@ +?gcc:usr/lib/libmesonstatic.a +?gcc:usr/lib/libmesonbothlib.a +?msvc:usr/lib/mesonstatic.lib +?msvc:usr/lib/mesonbothlib.lib +usr/bin/mesonbin?exe diff --git a/test cases/rust/7 cargo module/meson.build b/test cases/rust/7 cargo module/meson.build new file mode 100644 index 000000000000..dd92bd5ba4b4 --- /dev/null +++ b/test cases/rust/7 cargo module/meson.build @@ -0,0 +1,7 @@ +project('cargo module', 'c', 'rust') + +cargo = import('unstable-cargo') + +subdir('staticlib') +subdir('bothlibs') +subdir('bins') diff --git a/test cases/rust/7 cargo module/staticlib/Cargo.toml b/test cases/rust/7 cargo module/staticlib/Cargo.toml new file mode 100644 index 000000000000..4e1bfb794b5e --- /dev/null +++ b/test cases/rust/7 cargo module/staticlib/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "mesonstatic" +version = "0.1.0" +authors = ["Nirbheek Chauhan "] + +[dependencies] + +[lib] +crate-type = ['staticlib'] diff --git a/test cases/rust/7 cargo module/staticlib/meson.build b/test cases/rust/7 cargo module/staticlib/meson.build new file mode 100644 index 000000000000..b2d0f31ae573 --- /dev/null +++ b/test cases/rust/7 cargo module/staticlib/meson.build @@ -0,0 +1,3 @@ +cargo.static_library('mesonstatic', + toml : 'Cargo.toml', + install : true) diff --git a/test cases/rust/7 cargo module/staticlib/src/lib.rs b/test cases/rust/7 cargo module/staticlib/src/lib.rs new file mode 100644 index 000000000000..31e1bb209f98 --- /dev/null +++ b/test cases/rust/7 cargo module/staticlib/src/lib.rs @@ -0,0 +1,7 @@ +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + assert_eq!(2 + 2, 4); + } +}