diff options
48 files changed, 1058 insertions, 402 deletions
diff --git a/.github/workflows/os_comp.yml b/.github/workflows/os_comp.yml index 3cdcccd..5b49142 100644 --- a/.github/workflows/os_comp.yml +++ b/.github/workflows/os_comp.yml @@ -21,6 +21,7 @@ on: - "run_unittests.py" jobs: + arch: name: ${{ matrix.cfg.name }} runs-on: ubuntu-latest strategy: diff --git a/docs/markdown/Builtin-options.md b/docs/markdown/Builtin-options.md index ef327e3..2d7c01c 100644 --- a/docs/markdown/Builtin-options.md +++ b/docs/markdown/Builtin-options.md @@ -119,30 +119,36 @@ no options. The following options are available. Note that they may not be available on all platforms or with all compilers: -| Option | Default value | Possible values | Description | -| ----------- | ------------- | --------------- | ----------- | -| b_asneeded | true | true, false | Use -Wl,--as-needed when linking | -| b_bitcode | false | true, false | Embed Apple bitcode, see below | -| b_colorout | always | auto, always, never | Use colored output | -| b_coverage | false | true, false | Enable coverage tracking | -| b_lundef | true | true, false | Don't allow undefined symbols when linking | -| b_lto | false | true, false | Use link time optimization | -| b_ndebug | false | true, false, if-release | Disable asserts | -| b_pch | true | true, false | Use precompiled headers | -| b_pgo | off | off, generate, use | Use profile guided optimization | -| b_sanitize | none | see below | Code sanitizer to use | -| b_staticpic | true | true, false | Build static libraries as position independent | -| b_pie | false | true, false | Build position-independent executables (since 0.49.0)| -| b_vscrt | from_buildtype| none, md, mdd, mt, mtd, from_buildtype, static_from_buildtype | VS runtime library to use (since 0.48.0) (static_from_buildtype since 0.56.0) | +| Option | Default value | Possible values | Description | +|---------------|----------------|------------------------------------------------------------------|-------------------------------------------------------------------------------| +| b_asneeded | true | true, false | Use -Wl,--as-needed when linking | +| b_bitcode | false | true, false | Embed Apple bitcode, see below | +| b_colorout | always | auto, always, never | Use colored output | +| b_coverage | false | true, false | Enable coverage tracking | +| b_lundef | true | true, false | Don't allow undefined symbols when linking | +| b_lto | false | true, false | Use link time optimization | +| b_lto_threads | 0 | Any integer* | Use multiple threads for lto. *(Added in 0.57.0)* | +| b_lto_mode | default | default, thin | Select between lto modes, thin and default. *(Added in 0.57.0)* | +| b_ndebug | false | true, false, if-release | Disable asserts | +| b_pch | true | true, false | Use precompiled headers | +| b_pgo | off | off, generate, use | Use profile guided optimization | +| b_sanitize | none | see below | Code sanitizer to use | +| b_staticpic | true | true, false | Build static libraries as position independent | +| b_pie | false | true, false | Build position-independent executables (since 0.49.0) | +| b_vscrt | from_buildtype | none, md, mdd, mt, mtd, from_buildtype, static_from_buildtype | VS runtime library to use (since 0.48.0) (static_from_buildtype since 0.56.0) | The value of `b_sanitize` can be one of: `none`, `address`, `thread`, `undefined`, `memory`, `address,undefined`. -<a name="b_vscrt-from_buildtype"></a> The default value of `b_vscrt` -is `from_buildtype`. The following table is used internally to pick -the CRT compiler arguments for `from_buildtype` or -`static_from_buildtype` *(since 0.56)* based on the value of the -`buildtype` option: +* < 0 means disable, == 0 means automatic selection, > 0 sets a specific number to use + +LLVM supports `thin` lto, for more discussion see [LLVM's documentation](https://clang.llvm.org/docs/ThinLTO.html) + +<a name="b_vscrt-from_buildtype"></a> +The default value of `b_vscrt` is `from_buildtype`. The following table is +used internally to pick the CRT compiler arguments for `from_buildtype` or +`static_from_buildtype` *(since 0.56)* based on the value of the `buildtype` +option: | buildtype | from_buildtype | static_from_buildtype | | -------- | -------------- | --------------------- | diff --git a/docs/markdown/Fs-module.md b/docs/markdown/Fs-module.md index d4945e9..df9f305 100644 --- a/docs/markdown/Fs-module.md +++ b/docs/markdown/Fs-module.md @@ -199,3 +199,14 @@ suffix fs.stem('foo/bar/baz.dll') # baz fs.stem('foo/bar/baz.dll.a') # baz.dll ``` + +### read +- `read(path, encoding: 'utf-8')` *(since 0.57.0)*: + return a [string](Syntax.md#strings) with the contents of the given `path`. + If the `encoding` keyword argument is not specified, the file specified by + `path` is assumed to be utf-8 encoded. Binary files are not supported. The + provided paths should be relative to the current `meson.current_source_dir()` + or an absolute path outside the build directory is accepted. If the file + specified by `path` changes, this will trigger Meson to reconfigure the + project. If the file specified by `path` is a `files()` object it + cannot refer to a built file. diff --git a/docs/markdown/Gnome-module.md b/docs/markdown/Gnome-module.md index fd58d51..4088061 100644 --- a/docs/markdown/Gnome-module.md +++ b/docs/markdown/Gnome-module.md @@ -357,3 +357,20 @@ Takes as argument a module name and returns the path where that module's HTML files will be installed. Usually used with `install_data` to install extra files, such as images, to the output directory. + +### gnome.post_install() + +*Since 0.57.0* + +Post-install update of various system wide caches. Each script will be executed +only once even if `gnome.post_install()` is called multiple times from multiple +subprojects. If `DESTDIR` is specified during installation all scripts will be +skipped. + +It takes the following keyword arguments: +- `glib_compile_schemas`: If set to `true`, update `gschemas.compiled` file in + `<prefix>/<datadir>/glib-2.0/schemas`. +- `gio_querymodules`: List of directories relative to `prefix` where + `giomodule.cache` file will be updated. +- `gtk_update_icon_cache`: If set to `true`, update `icon-theme.cache` file in + `<prefix>/<datadir>/icons/hicolor`. diff --git a/docs/markdown/Reference-manual.md b/docs/markdown/Reference-manual.md index d3a4f01..f12f695 100644 --- a/docs/markdown/Reference-manual.md +++ b/docs/markdown/Reference-manual.md @@ -1507,6 +1507,10 @@ and subdirectory the target was defined in, respectively. - `depends` is a list of targets that this target depends on but which are not listed in the command array (because, for example, the script does file globbing internally) +- `env` *(since 0.57.0)*: environment variables to set, such as + `{'NAME1': 'value1', 'NAME2': 'value2'}` or `['NAME1=value1', 'NAME2=value2']`, + or an [`environment()` object](#environment-object) which allows more + sophisticated environment juggling. ### set_variable() @@ -1839,14 +1843,14 @@ the following methods. script file can not be found in the staging directory, it is a hard error. This command can only invoked from the main project, calling it from a subproject is a hard error. *(since 0.49.0)* Accepts multiple arguments - for the fscript. *(since 0.54.0)* The `MESON_SOURCE_ROOT` and `MESON_BUILD_ROOT` + for the script. *(since 0.54.0)* The `MESON_SOURCE_ROOT` and `MESON_BUILD_ROOT` environment variables are set when dist scripts are run. *(since 0.55.0)* The output of `configure_file`, `files`, and `find_program` as well as strings. *(since 0.57.0)* `file` objects and the output of `configure_file` may be - *used as the `script_name` parameter. + used as the `script_name` parameter. - `add_install_script(script_name, arg1, arg2, ...)`: causes the script given as an argument to be run during the install step, this script diff --git a/docs/markdown/Reference-tables.md b/docs/markdown/Reference-tables.md index 806ba76..256aca4 100644 --- a/docs/markdown/Reference-tables.md +++ b/docs/markdown/Reference-tables.md @@ -89,6 +89,7 @@ set in the cross file. | dspic | 16 bit Microchip dsPIC | | e2k | MCST Elbrus processor | | ia64 | Itanium processor | +| loongarch64 | 64 bit Loongson processor| | m68k | Motorola 68000 processor | | microblaze | MicroBlaze processor | | mips | 32 bit MIPS processor | diff --git a/docs/markdown/howtox.md b/docs/markdown/howtox.md index f05f3e8..8c8c0c0 100644 --- a/docs/markdown/howtox.md +++ b/docs/markdown/howtox.md @@ -139,6 +139,23 @@ cdata.set('SOMETHING', txt) configure_file(...) ``` +## Generate configuration data from files + +`The [fs module](#Fs-modules) offers the `read` function` which enables adding +the contents of arbitrary files to configuration data (among other uses): + +```meson +fs = import('fs') +cdata = configuration_data() +copyright = fs.read('LICENSE') +cdata.set('COPYRIGHT', copyright) +if build_machine.system() == 'linux' + os_release = fs.read('/etc/os-release') + cdata.set('LINUX_BUILDER', os_release) +endif +configure_file(...) +``` + ## Generate a runnable script with `configure_file` `configure_file` preserves metadata so if your template file has diff --git a/docs/markdown/snippets/customtarget_env.md b/docs/markdown/snippets/customtarget_env.md index 69bfc0d..f2d651b 100644 --- a/docs/markdown/snippets/customtarget_env.md +++ b/docs/markdown/snippets/customtarget_env.md @@ -1,4 +1,4 @@ -## `custom_target()` now accepts an `env` keyword argument +## `custom_target()` and `run_target()` now accepts an `env` keyword argument Environment variables can now be passed to the `custom_target()` command. diff --git a/docs/markdown/snippets/fs_read.md b/docs/markdown/snippets/fs_read.md new file mode 100644 index 0000000..05c215a --- /dev/null +++ b/docs/markdown/snippets/fs_read.md @@ -0,0 +1,40 @@ +## Support for reading files at configuration time with the `fs` module + +Reading text files during configuration is now supported. This can be done at +any time after `project` has been called + +```meson +project(myproject', 'c') +license_text = run_command( + find_program('python3'), '-c', 'print(open("COPYING").read())' +).stdout().strip() +about_header = configuration_data() +about_header.add('COPYRIGHT', license_text) +about_header.add('ABOUT_STRING', meson.project_name()) +... +``` + +There are several problems with the above approach: +1. It's ugly and confusing +2. If `COPYING` changes after configuration, Meson won't correctly rebuild when + configuration data is based on the data in COPYING +3. It has extra overhead + +`fs.read` replaces the above idiom thus: +```meson +project(myproject', 'c') +fs = import('fs') +license_text = fs.read('COPYING').strip() +about_header = configuration_data() +about_header.add('COPYRIGHT', license_text) +about_header.add('ABOUT_STRING', meson.project_name()) +... +``` + +They are not equivalent, though. Files read with `fs.read` create a +configuration dependency on the file, and so if the `COPYING` file is modified, +Meson will automatically reconfigure, guaranteeing the build is consistent. It +can be used for any properly encoded text files. It supports specification of +non utf-8 encodings too, so if you're stuck with text files in a different +encoding, it can be passed as an argument. See the [`meson` +object](Reference-manual.md#meson-object) documentation for details. diff --git a/docs/markdown/snippets/gnome_install_script.md b/docs/markdown/snippets/gnome_install_script.md new file mode 100644 index 0000000..03fcfe4 --- /dev/null +++ b/docs/markdown/snippets/gnome_install_script.md @@ -0,0 +1,9 @@ +## `gnome.post_install()` + +Post-install update of various system wide caches. Each script will be executed +only once even if `gnome.post_install()` is called multiple times from multiple +subprojects. If `DESTDIR` is specified during installation all scripts will be +skipped. + +Currently supports `glib-compile-schemas`, `gio-querymodules`, and +`gtk-update-icon-cache`. diff --git a/docs/markdown/snippets/install_dry_run.md b/docs/markdown/snippets/install_dry_run.md new file mode 100644 index 0000000..8106a06 --- /dev/null +++ b/docs/markdown/snippets/install_dry_run.md @@ -0,0 +1,4 @@ +## meson install --dry-run + +New option to meson install command that does not actually install files, but +only print messages. diff --git a/docs/markdown/snippets/lto_mode.md b/docs/markdown/snippets/lto_mode.md new file mode 100644 index 0000000..c1df066 --- /dev/null +++ b/docs/markdown/snippets/lto_mode.md @@ -0,0 +1,5 @@ +## Support added for LLVM's thinLTO + +A new `b_lto_mode` option has been added, which may be set to `default` or +`thin`. Thin only works for clang, and only with gnu gold, lld variants, or +ld64. diff --git a/docs/markdown/snippets/lto_threads.md b/docs/markdown/snippets/lto_threads.md new file mode 100644 index 0000000..77c8047 --- /dev/null +++ b/docs/markdown/snippets/lto_threads.md @@ -0,0 +1,7 @@ +## Knob to control LTO thread + +Both the gnu linker and lld support using threads for speeding up LTO, meson +now provides a knob for this: `-Db_lto_threads`. Currently this is only +supported for clang and gcc. Any positive integer is supported, `0` means +`auto`. If the compiler or linker implements it's on `auto` we use that, +otherwise the number of threads on the machine is used. diff --git a/docs/markdown/snippets/meson_test_depends.md b/docs/markdown/snippets/meson_test_depends.md index 905c59f..46ec328 100644 --- a/docs/markdown/snippets/meson_test_depends.md +++ b/docs/markdown/snippets/meson_test_depends.md @@ -12,5 +12,5 @@ using commands like the following: This would find the broken commit automatically while at each step rebuilding only those pieces of code needed to run the test. -However, this change could cause failures when upgrading to 0.57, ifthe +However, this change could cause failures when upgrading to 0.57, if the dependencies are not specified correctly in `meson.build`. diff --git a/docs/markdown/snippets/unstable-rust-module.md b/docs/markdown/snippets/unstable-rust-module.md index 47790e3..e594ecf 100644 --- a/docs/markdown/snippets/unstable-rust-module.md +++ b/docs/markdown/snippets/unstable-rust-module.md @@ -1,4 +1,4 @@ -## Untable Rust module +## Unstable Rust module A new unstable module has been added to make using Rust with Meson easier. Currently it adds a single function to ease defining Rust tests. diff --git a/docs/markdown/snippets/vala_unity_builds_disabled.md b/docs/markdown/snippets/vala_unity_builds_disabled.md new file mode 100644 index 0000000..80e6523 --- /dev/null +++ b/docs/markdown/snippets/vala_unity_builds_disabled.md @@ -0,0 +1,7 @@ +## Unity build with Vala disabled + +The approach that meson has used for Vala unity builds is incorrect, we +combine the generated C files like we would any other C file. This is very +fragile however, as the Vala compiler generates helper functions and macros +which work fine when each file is a separate translation unit, but fail when +they are combined. diff --git a/mesonbuild/backend/backends.py b/mesonbuild/backend/backends.py index badc2d0..e19afca 100644 --- a/mesonbuild/backend/backends.py +++ b/mesonbuild/backend/backends.py @@ -21,8 +21,6 @@ import json import os import pickle import re -import shlex -import textwrap import typing as T import hashlib import copy @@ -34,7 +32,7 @@ from .. import mlog from ..compilers import LANGUAGES_USING_LDFLAGS from ..mesonlib import ( File, MachineChoice, MesonException, OptionType, OrderedSet, OptionOverrideProxy, - classify_unity_sources, unholder, OptionKey + classify_unity_sources, unholder, OptionKey, join_args ) if T.TYPE_CHECKING: @@ -49,7 +47,7 @@ if T.TYPE_CHECKING: # Languages that can mix with C or C++ but don't support unity builds yet # because the syntax we use for unity builds is specific to C/++/ObjC/++. # Assembly files cannot be unitified and neither can LLVM IR files -LANGS_CANT_UNITY = ('d', 'fortran') +LANGS_CANT_UNITY = ('d', 'fortran', 'vala') class TestProtocol(enum.Enum): @@ -138,6 +136,7 @@ class ExecutableSerialisation: self.capture = capture self.pickled = False self.skip_if_destdir = False + self.verbose = False class TestSerialisation: def __init__(self, name: str, project: str, suite: str, fname: T.List[str], @@ -248,6 +247,17 @@ class Backend: return self.environment.coredata.validate_option_value(option_name, override) return self.environment.coredata.get_option(option_name.evolve(subproject=target.subproject)) + def get_source_dir_include_args(self, target, compiler): + curdir = target.get_subdir() + tmppath = os.path.normpath(os.path.join(self.build_to_src, curdir)) + return compiler.get_include_args(tmppath, False) + + def get_build_dir_include_args(self, target, compiler): + curdir = target.get_subdir() + if curdir == '': + curdir = '.' + return compiler.get_include_args(curdir, False) + def get_target_filename_for_linking(self, target): # On some platforms (msvc for instance), the file that is used for # dynamic linking is not the same as the dynamic library itself. This @@ -421,12 +431,14 @@ class Backend: def as_meson_exe_cmdline(self, tname, exe, cmd_args, workdir=None, extra_bdeps=None, capture=None, force_serialize=False, - env: T.Optional[build.EnvironmentVariables] = None): + env: T.Optional[build.EnvironmentVariables] = None, + verbose: bool = False): ''' Serialize an executable for running with a generator or a custom target ''' cmd = [exe] + cmd_args es = self.get_executable_serialisation(cmd, workdir, extra_bdeps, capture, env) + es.verbose = verbose reasons = [] if es.extra_paths: reasons.append('to set PATH') @@ -976,18 +988,8 @@ class Backend: if delta > 0.001: raise MesonException('Clock skew detected. File {} has a time stamp {:.4f}s in the future.'.format(absf, delta)) - def build_target_to_cmd_array(self, bt, check_cross): + def build_target_to_cmd_array(self, bt): if isinstance(bt, build.BuildTarget): - if check_cross and isinstance(bt, build.Executable) and bt.for_machine is not MachineChoice.BUILD: - if (self.environment.is_cross_build() and - self.environment.exe_wrapper is None and - self.environment.need_exe_wrapper()): - s = textwrap.dedent(''' - Cannot use target {} as a generator because it is built for the - host machine and no exe wrapper is defined or needs_exe_wrapper is - true. You might want to set `native: true` instead to build it for - the build machine.'''.format(bt.name)) - raise MesonException(s) arr = [os.path.join(self.environment.get_build_dir(), self.get_target_filename(bt))] else: arr = bt.get_command() @@ -1118,11 +1120,9 @@ class Backend: inputs = self.get_custom_target_sources(target) # Evaluate the command list cmd = [] - index = -1 for i in target.command: - index += 1 if isinstance(i, build.BuildTarget): - cmd += self.build_target_to_cmd_array(i, (index == 0)) + cmd += self.build_target_to_cmd_array(i) continue elif isinstance(i, build.CustomTarget): # GIR scanner will attempt to execute this binary but @@ -1135,10 +1135,7 @@ class Backend: i = os.path.join(self.environment.get_build_dir(), i) # FIXME: str types are blindly added ignoring 'target.absolute_paths' # because we can't know if they refer to a file or just a string - elif not isinstance(i, str): - err_msg = 'Argument {0} is of unknown type {1}' - raise RuntimeError(err_msg.format(str(i), str(type(i)))) - else: + elif isinstance(i, str): if '@SOURCE_ROOT@' in i: i = i.replace('@SOURCE_ROOT@', source_root) if '@BUILD_ROOT@' in i: @@ -1168,6 +1165,9 @@ class Backend: else: lead_dir = self.environment.get_build_dir() i = i.replace(source, os.path.join(lead_dir, outdir)) + else: + err_msg = 'Argument {0} is of unknown type {1}' + raise RuntimeError(err_msg.format(str(i), str(type(i)))) cmd.append(i) # Substitute the rest of the template strings values = mesonlib.get_filenames_templates_dict(inputs, outputs) @@ -1193,11 +1193,21 @@ class Backend: cmd = [i.replace('\\', '/') for i in cmd] return inputs, outputs, cmd + def get_run_target_env(self, target: build.RunTarget) -> build.EnvironmentVariables: + env = target.env if target.env else build.EnvironmentVariables() + introspect_cmd = join_args(self.environment.get_build_command() + ['introspect']) + env.add_var(env.set, 'MESON_SOURCE_ROOT', [self.environment.get_source_dir()], {}) + env.add_var(env.set, 'MESON_BUILD_ROOT', [self.environment.get_build_dir()], {}) + env.add_var(env.set, 'MESON_SUBDIR', [target.subdir], {}) + env.add_var(env.set, 'MESONINTROSPECT', [introspect_cmd], {}) + return env + def run_postconf_scripts(self) -> None: from ..scripts.meson_exe import run_exe + introspect_cmd = join_args(self.environment.get_build_command() + ['introspect']) env = {'MESON_SOURCE_ROOT': self.environment.get_source_dir(), 'MESON_BUILD_ROOT': self.environment.get_build_dir(), - 'MESONINTROSPECT': ' '.join([shlex.quote(x) for x in self.environment.get_build_command() + ['introspect']]), + 'MESONINTROSPECT': introspect_cmd, } for s in self.build.postconf_scripts: diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py index 36f1fd2..3eca3c0 100644 --- a/mesonbuild/backend/ninjabackend.py +++ b/mesonbuild/backend/ninjabackend.py @@ -28,7 +28,6 @@ from .. import modules from .. import environment, mesonlib from .. import build from .. import mlog -from .. import dependencies from .. import compilers from ..arglist import CompilerArgs from ..compilers import ( @@ -812,25 +811,24 @@ int dummy; # Generate compilation targets for C sources generated from Vala # sources. This can be extended to other $LANG->C compilers later if # necessary. This needs to be separate for at least Vala + # + # Do not try to unity-build the generated c files from vala, as these + # often contain duplicate symbols and will fail to compile properly vala_generated_source_files = [] for src in vala_generated_sources: dirpart, fnamepart = os.path.split(src) raw_src = File(True, dirpart, fnamepart) - if is_unity: - unity_src.append(os.path.join(self.environment.get_build_dir(), src)) + # Generated targets are ordered deps because the must exist + # before the sources compiling them are used. After the first + # compile we get precise dependency info from dep files. + # This should work in all cases. If it does not, then just + # move them from orderdeps to proper deps. + if self.environment.is_header(src): header_deps.append(raw_src) else: - # Generated targets are ordered deps because the must exist - # before the sources compiling them are used. After the first - # compile we get precise dependency info from dep files. - # This should work in all cases. If it does not, then just - # move them from orderdeps to proper deps. - if self.environment.is_header(src): - header_deps.append(raw_src) - else: - # We gather all these and generate compile rules below - # after `header_deps` (above) is fully generated - vala_generated_source_files.append(raw_src) + # We gather all these and generate compile rules below + # after `header_deps` (above) is fully generated + vala_generated_source_files.append(raw_src) for src in vala_generated_source_files: # Passing 'vala' here signifies that we want the compile # arguments to be specialized for C code generated by @@ -975,7 +973,6 @@ int dummy; elem.add_item('DEPFILE', rel_dfile) if target.console: elem.add_item('pool', 'console') - cmd = self.replace_paths(target, cmd) elem.add_item('COMMAND', cmd) elem.add_item('description', desc.format(target.name, cmd_type)) self.add_build(elem) @@ -989,65 +986,28 @@ int dummy; return '{}{}'.format(subproject_prefix, target.name) def generate_run_target(self, target): - cmd = self.environment.get_build_command() + ['--internal', 'commandrunner'] - deps = self.unwrap_dep_list(target) - arg_strings = [] - for i in target.args: - if isinstance(i, str): - arg_strings.append(i) - elif isinstance(i, (build.BuildTarget, build.CustomTarget)): - relfname = self.get_target_filename(i) - arg_strings.append(os.path.join(self.environment.get_build_dir(), relfname)) - deps.append(relfname) - elif isinstance(i, mesonlib.File): - relfname = i.rel_to_builddir(self.build_to_src) - arg_strings.append(os.path.join(self.environment.get_build_dir(), relfname)) - else: - raise AssertionError('Unreachable code in generate_run_target: ' + str(i)) - cmd += [self.environment.get_source_dir(), - self.environment.get_build_dir(), - target.subdir] + self.environment.get_build_command() - texe = target.command - try: - texe = texe.held_object - except AttributeError: - pass - if isinstance(texe, build.Executable): - abs_exe = os.path.join(self.environment.get_build_dir(), self.get_target_filename(texe)) - deps.append(self.get_target_filename(texe)) - if self.environment.is_cross_build(): - exe_wrap = self.environment.get_exe_wrapper() - if exe_wrap: - if not exe_wrap.found(): - msg = 'The exe_wrapper {!r} defined in the cross file is ' \ - 'needed by run target {!r}, but was not found. ' \ - 'Please check the command and/or add it to PATH.' - raise MesonException(msg.format(exe_wrap.name, target.name)) - cmd += exe_wrap.get_command() - cmd.append(abs_exe) - elif isinstance(texe, dependencies.ExternalProgram): - cmd += texe.get_command() - elif isinstance(texe, build.CustomTarget): - deps.append(self.get_target_filename(texe)) - cmd += [os.path.join(self.environment.get_build_dir(), self.get_target_filename(texe))] - elif isinstance(texe, mesonlib.File): - cmd.append(texe.absolute_path(self.environment.get_source_dir(), self.environment.get_build_dir())) + target_name = self.build_run_target_name(target) + if not target.command: + # This is an alias target, it has no command, it just depends on + # other targets. + elem = NinjaBuildElement(self.all_outputs, target_name, 'phony', []) else: - cmd.append(target.command) - cmd += arg_strings - - if texe: - target_name = 'meson-{}'.format(self.build_run_target_name(target)) - elem = NinjaBuildElement(self.all_outputs, target_name, 'CUSTOM_COMMAND', []) - elem.add_item('COMMAND', cmd) - elem.add_item('description', 'Running external command {}'.format(target.name)) + target_env = self.get_run_target_env(target) + _, _, cmd = self.eval_custom_target_command(target) + desc = 'Running external command {}{}' + meson_exe_cmd, reason = self.as_meson_exe_cmdline(target_name, cmd[0], cmd[1:], + force_serialize=True, env=target_env, + verbose=True) + cmd_type = ' (wrapped by meson {})'.format(reason) + internal_target_name = 'meson-{}'.format(target_name) + elem = NinjaBuildElement(self.all_outputs, internal_target_name, 'CUSTOM_COMMAND', []) + elem.add_item('COMMAND', meson_exe_cmd) + elem.add_item('description', desc.format(target.name, cmd_type)) elem.add_item('pool', 'console') # Alias that runs the target defined above with the name the user specified - self.create_target_alias(target_name) - else: - target_name = self.build_run_target_name(target) - elem = NinjaBuildElement(self.all_outputs, target_name, 'phony', []) - + self.create_target_alias(internal_target_name) + deps = self.unwrap_dep_list(target) + deps += self.get_custom_target_depend_files(target) elem.add_dep(deps) self.add_build(elem) self.processed_targets[target.get_id()] = True @@ -1396,7 +1356,9 @@ int dummy; break return list(result) - def split_vala_sources(self, t): + def split_vala_sources(self, t: build.Target) -> \ + T.Tuple[T.MutableMapping[str, File], T.MutableMapping[str, File], + T.Tuple[T.MutableMapping[str, File], T.MutableMapping]]: """ Splits the target's sources into .vala, .gs, .vapi, and other sources. Handles both pre-existing and generated sources. @@ -1405,9 +1367,9 @@ int dummy; the keys being the path to the file (relative to the build directory) and the value being the object that generated or represents the file. """ - vala = OrderedDict() - vapi = OrderedDict() - others = OrderedDict() + vala: T.MutableMapping[str, File] = OrderedDict() + vapi: T.MutableMapping[str, File] = OrderedDict() + others: T.MutableMapping[str, File] = OrderedDict() othersgen = OrderedDict() # Split pre-existing sources for s in t.get_sources(): @@ -1449,7 +1411,7 @@ int dummy; srctype[f] = gensrc return vala, vapi, (others, othersgen) - def generate_vala_compile(self, target): + def generate_vala_compile(self, target: build.BuildTarget): """Vala is compiled into C. Set up all necessary build steps here.""" (vala_src, vapi_src, other_src) = self.split_vala_sources(target) extra_dep_files = [] @@ -2104,7 +2066,7 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485''')) generator = genlist.get_generator() subdir = genlist.subdir exe = generator.get_exe() - exe_arr = self.build_target_to_cmd_array(exe, True) + exe_arr = self.build_target_to_cmd_array(exe) infilelist = genlist.get_inputs() outfilelist = genlist.get_outputs() extra_dependencies = self.get_custom_target_depend_files(genlist) @@ -2334,17 +2296,6 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485''')) self.add_build(element) return (rel_obj, rel_src) - def get_source_dir_include_args(self, target, compiler): - curdir = target.get_subdir() - tmppath = os.path.normpath(os.path.join(self.build_to_src, curdir)) - return compiler.get_include_args(tmppath, False) - - def get_build_dir_include_args(self, target, compiler): - curdir = target.get_subdir() - if curdir == '': - curdir = '.' - return compiler.get_include_args(curdir, False) - @lru_cache(maxsize=None) def get_normpath_target(self, source) -> str: return os.path.normpath(source) diff --git a/mesonbuild/backend/vs2010backend.py b/mesonbuild/backend/vs2010backend.py index c47fb4a..e94ab49 100644 --- a/mesonbuild/backend/vs2010backend.py +++ b/mesonbuild/backend/vs2010backend.py @@ -28,7 +28,7 @@ from .. import mlog from .. import compilers from ..interpreter import Interpreter from ..mesonlib import ( - MesonException, File, python_command, replace_if_different, OptionKey, + MesonException, python_command, replace_if_different, OptionKey, ) from ..environment import Environment, build_filename @@ -121,7 +121,7 @@ class Vs2010Backend(backends.Backend): infilelist = genlist.get_inputs() outfilelist = genlist.get_outputs() source_dir = os.path.join(down, self.build_to_src, genlist.subdir) - exe_arr = self.build_target_to_cmd_array(exe, True) + exe_arr = self.build_target_to_cmd_array(exe) idgroup = ET.SubElement(parent_node, 'ItemGroup') for i in range(len(infilelist)): if len(infilelist) == len(outfilelist): @@ -257,9 +257,8 @@ class Vs2010Backend(backends.Backend): for d in target.get_target_dependencies(): all_deps[d.get_id()] = d elif isinstance(target, build.RunTarget): - for d in [target.command] + target.args: - if isinstance(d, (build.BuildTarget, build.CustomTarget)): - all_deps[d.get_id()] = d + for d in target.get_dependencies(): + all_deps[d.get_id()] = d elif isinstance(target, build.BuildTarget): for ldep in target.link_targets: if isinstance(ldep, build.CustomTargetIndex): @@ -534,27 +533,14 @@ class Vs2010Backend(backends.Backend): # is probably a better way than running a this dummy command. cmd_raw = python_command + ['-c', 'exit'] else: - cmd_raw = [target.command] + target.args - cmd = python_command + \ - [os.path.join(self.environment.get_script_dir(), 'commandrunner.py'), - self.environment.get_source_dir(), - self.environment.get_build_dir(), - self.get_target_dir(target)] + self.environment.get_build_command() - for i in cmd_raw: - if isinstance(i, build.BuildTarget): - cmd.append(os.path.join(self.environment.get_build_dir(), self.get_target_filename(i))) - elif isinstance(i, dependencies.ExternalProgram): - cmd += i.get_command() - elif isinstance(i, File): - relfname = i.rel_to_builddir(self.build_to_src) - cmd.append(os.path.join(self.environment.get_build_dir(), relfname)) - elif isinstance(i, str): - # Escape embedded quotes, because we quote the entire argument below. - cmd.append(i.replace('"', '\\"')) - else: - cmd.append(i) - cmd_templ = '''"%s" ''' * len(cmd) - self.add_custom_build(root, 'run_target', cmd_templ % tuple(cmd)) + _, _, cmd_raw = self.eval_custom_target_command(target) + depend_files = self.get_custom_target_depend_files(target) + target_env = self.get_run_target_env(target) + wrapper_cmd, _ = self.as_meson_exe_cmdline(target.name, cmd_raw[0], cmd_raw[1:], + force_serialize=True, env=target_env, + verbose=True) + self.add_custom_build(root, 'run_target', ' '.join(self.quote_arguments(wrapper_cmd)), + deps=depend_files) ET.SubElement(root, 'Import', Project=r'$(VCTargetsPath)\Microsoft.Cpp.targets') self.add_regen_dependency(root) self.add_target_deps(root, target) diff --git a/mesonbuild/backend/xcodebackend.py b/mesonbuild/backend/xcodebackend.py index 09a40ef..7ee4e80 100644 --- a/mesonbuild/backend/xcodebackend.py +++ b/mesonbuild/backend/xcodebackend.py @@ -734,6 +734,9 @@ class XCodeBackend(backends.Backend): else: product_name = target.get_basename() ldargs += target.link_args + linker, stdlib_args = self.determine_linker_and_stdlib_args(target) + ldargs += self.build.get_project_link_args(linker, target.subproject, target.for_machine) + ldargs += self.build.get_global_link_args(linker, target.for_machine) cargs = [] for dep in target.get_external_deps(): cargs += dep.get_compile_args() @@ -752,8 +755,13 @@ class XCodeBackend(backends.Backend): targs = target.get_extra_args(lang) args = pargs + gargs + targs if args: - langargs[langnamemap[lang]] = args - langargs['C'] += cargs + langname = langnamemap[lang] + compiler = target.compilers.get(lang) + lang_cargs = cargs + if compiler and target.implicit_include_directories: + lang_cargs += self.get_build_dir_include_args(target, compiler) + langargs[langname] = args + langargs[langname] += lang_cargs symroot = os.path.join(self.environment.get_build_dir(), target.subdir) self.write_line('%s /* %s */ = {' % (valid, buildtype)) self.indent_level += 1 diff --git a/mesonbuild/build.py b/mesonbuild/build.py index 5adbc51..160ee9a 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -29,7 +29,7 @@ from .mesonlib import ( File, MesonException, MachineChoice, PerMachine, OrderedSet, listify, extract_as_list, typeslistify, stringlistify, classify_unity_sources, get_filenames_templates_dict, substitute_values, has_path_sep, unholder, - OptionKey, + OptionKey ) from .compilers import ( Compiler, is_object, clink_langs, sort_clink, lang_suffixes, @@ -2141,8 +2141,35 @@ class SharedModule(SharedLibrary): def get_default_install_dir(self, environment): return environment.get_shared_module_dir() +class CommandBase: + def flatten_command(self, cmd): + cmd = unholder(listify(cmd)) + final_cmd = [] + for c in cmd: + if isinstance(c, str): + final_cmd.append(c) + elif isinstance(c, File): + self.depend_files.append(c) + final_cmd.append(c) + elif isinstance(c, dependencies.ExternalProgram): + if not c.found(): + raise InvalidArguments('Tried to use not-found external program in "command"') + path = c.get_path() + if os.path.isabs(path): + # Can only add a dependency on an external program which we + # know the absolute path of + self.depend_files.append(File.from_absolute_file(path)) + final_cmd += c.get_command() + elif isinstance(c, (BuildTarget, CustomTarget)): + self.dependencies.append(c) + final_cmd.append(c) + elif isinstance(c, list): + final_cmd += self.flatten_command(c) + else: + raise InvalidArguments('Argument {!r} in "command" is invalid'.format(c)) + return final_cmd -class CustomTarget(Target): +class CustomTarget(Target, CommandBase): known_kwargs = set([ 'input', 'output', @@ -2213,33 +2240,6 @@ class CustomTarget(Target): bdeps.update(d.get_transitive_build_target_deps()) return bdeps - def flatten_command(self, cmd): - cmd = unholder(listify(cmd)) - final_cmd = [] - for c in cmd: - if isinstance(c, str): - final_cmd.append(c) - elif isinstance(c, File): - self.depend_files.append(c) - final_cmd.append(c) - elif isinstance(c, dependencies.ExternalProgram): - if not c.found(): - raise InvalidArguments('Tried to use not-found external program in "command"') - path = c.get_path() - if os.path.isabs(path): - # Can only add a dependency on an external program which we - # know the absolute path of - self.depend_files.append(File.from_absolute_file(path)) - final_cmd += c.get_command() - elif isinstance(c, (BuildTarget, CustomTarget)): - self.dependencies.append(c) - final_cmd.append(c) - elif isinstance(c, list): - final_cmd += self.flatten_command(c) - else: - raise InvalidArguments('Argument {!r} in "command" is invalid'.format(c)) - return final_cmd - def process_kwargs(self, kwargs, backend): self.process_kwargs_base(kwargs) self.sources = unholder(extract_as_list(kwargs, 'input')) @@ -2421,18 +2421,20 @@ class CustomTarget(Target): for i in self.outputs: yield CustomTargetIndex(self, i) -class RunTarget(Target): - def __init__(self, name, command, args, dependencies, subdir, subproject): +class RunTarget(Target, CommandBase): + def __init__(self, name, command, dependencies, subdir, subproject, env=None): self.typename = 'run' # These don't produce output artifacts super().__init__(name, subdir, subproject, False, MachineChoice.BUILD) - self.command = command - self.args = args self.dependencies = dependencies + self.depend_files = [] + self.command = self.flatten_command(command) + self.absolute_paths = False + self.env = env def __repr__(self): repr_str = "<{0} {1}: {2}>" - return repr_str.format(self.__class__.__name__, self.get_id(), self.command) + return repr_str.format(self.__class__.__name__, self.get_id(), self.command[0]) def process_kwargs(self, kwargs): return self.process_kwargs_base(kwargs) @@ -2465,7 +2467,7 @@ class RunTarget(Target): class AliasTarget(RunTarget): def __init__(self, name, dependencies, subdir, subproject): - super().__init__(name, '', [], dependencies, subdir, subproject) + super().__init__(name, [], dependencies, subdir, subproject) class Jar(BuildTarget): known_kwargs = known_jar_kwargs diff --git a/mesonbuild/compilers/compilers.py b/mesonbuild/compilers/compilers.py index 0f83f4c..08db6d7 100644 --- a/mesonbuild/compilers/compilers.py +++ b/mesonbuild/compilers/compilers.py @@ -268,6 +268,11 @@ clike_debug_args = {False: [], base_options: 'KeyedOptionDictType' = { OptionKey('b_pch'): coredata.UserBooleanOption('Use precompiled headers', True), OptionKey('b_lto'): coredata.UserBooleanOption('Use link time optimization', False), + OptionKey('b_lto'): coredata.UserBooleanOption('Use link time optimization', False), + OptionKey('b_lto_threads'): coredata.UserIntegerOption('Use multiple threads for Link Time Optimization', (None, None,0)), + OptionKey('b_lto_mode'): coredata.UserComboOption('Select between different LTO modes.', + ['default', 'thin'], + 'default'), OptionKey('b_sanitize'): coredata.UserComboOption('Code sanitizer to use', ['none', 'address', 'thread', 'undefined', 'memory', 'address,undefined'], 'none'), @@ -300,11 +305,26 @@ def option_enabled(boptions: T.Set[OptionKey], options: 'KeyedOptionDictType', except KeyError: return False + +def get_option_value(options: 'KeyedOptionDictType', opt: OptionKey, fallback: '_T') -> '_T': + """Get the value of an option, or the fallback value.""" + try: + v: '_T' = options[opt].value + except KeyError: + return fallback + + assert isinstance(v, type(fallback)), f'Should have {type(fallback)!r} but was {type(v)!r}' + # Mypy doesn't understand that the above assert ensures that v is type _T + return v + + def get_base_compile_args(options: 'KeyedOptionDictType', compiler: 'Compiler') -> T.List[str]: args = [] # type T.List[str] try: if options[OptionKey('b_lto')].value: - args.extend(compiler.get_lto_compile_args()) + args.extend(compiler.get_lto_compile_args( + threads=get_option_value(options, OptionKey('b_lto_threads'), 0), + mode=get_option_value(options, OptionKey('b_lto_mode'), 'default'))) except KeyError: pass try: @@ -926,7 +946,7 @@ class Compiler(metaclass=abc.ABCMeta): ret.append(arg) return ret - def get_lto_compile_args(self) -> T.List[str]: + def get_lto_compile_args(self, *, threads: int = 0, mode: str = 'default') -> T.List[str]: return [] def get_lto_link_args(self) -> T.List[str]: diff --git a/mesonbuild/compilers/mixins/clang.py b/mesonbuild/compilers/mixins/clang.py index fcb2225..1778c31 100644 --- a/mesonbuild/compilers/mixins/clang.py +++ b/mesonbuild/compilers/mixins/clang.py @@ -19,7 +19,7 @@ import shutil import typing as T from ... import mesonlib -from ...linkers import AppleDynamicLinker +from ...linkers import AppleDynamicLinker, ClangClDynamicLinker, LLVMDynamicLinker, GnuGoldDynamicLinker from ...mesonlib import OptionKey from ..compilers import CompileCheckMode from .gnu import GnuLikeCompiler @@ -49,7 +49,9 @@ class ClangCompiler(GnuLikeCompiler): super().__init__() self.id = 'clang' self.defines = defines or {} - self.base_options.add(OptionKey('b_colorout')) + self.base_options.update( + {OptionKey('b_colorout'), OptionKey('b_lto_threads'), OptionKey('b_lto_mode')}) + # TODO: this really should be part of the linker base_options, but # linkers don't have base_options. if isinstance(self.linker, AppleDynamicLinker): @@ -135,3 +137,18 @@ class ClangCompiler(GnuLikeCompiler): def get_coverage_link_args(self) -> T.List[str]: return ['--coverage'] + + def get_lto_compile_args(self, *, threads: int = 0, mode: str = 'default') -> T.List[str]: + args: T.List[str] = [] + if mode == 'thin': + # Thin LTO requires the use of gold, lld, ld64, or lld-link + if not isinstance(self.linker, (AppleDynamicLinker, ClangClDynamicLinker, LLVMDynamicLinker, GnuGoldDynamicLinker)): + raise mesonlib.MesonException(f"LLVM's thinLTO only works with gnu gold, lld, lld-link, and ld64, not {self.linker.id}") + args.append(f'-flto={mode}') + else: + assert mode == 'default', 'someone forgot to wire something up' + args.extend(super().get_lto_compile_args(threads=threads)) + # In clang -flto=0 means auto + if threads >= 0: + args.append(f'-flto-jobs={threads}') + return args diff --git a/mesonbuild/compilers/mixins/gnu.py b/mesonbuild/compilers/mixins/gnu.py index 95bcd7c..464c664 100644 --- a/mesonbuild/compilers/mixins/gnu.py +++ b/mesonbuild/compilers/mixins/gnu.py @@ -17,6 +17,7 @@ import abc import functools import os +import multiprocessing import pathlib import re import subprocess @@ -281,7 +282,9 @@ class GnuLikeCompiler(Compiler, metaclass=abc.ABCMeta): return self._split_fetch_real_dirs(line.split('=', 1)[1]) return [] - def get_lto_compile_args(self) -> T.List[str]: + def get_lto_compile_args(self, *, threads: int = 0, mode: str = 'default') -> T.List[str]: + # This provides a base for many compilers, GCC and Clang override this + # for their specific arguments return ['-flto'] def sanitizer_compile_args(self, value: str) -> T.List[str]: @@ -330,7 +333,7 @@ class GnuCompiler(GnuLikeCompiler): super().__init__() self.id = 'gcc' self.defines = defines or {} - self.base_options.add(OptionKey('b_colorout')) + self.base_options.update({OptionKey('b_colorout'), OptionKey('b_lto_threads')}) def get_colorout_args(self, colortype: str) -> T.List[str]: if mesonlib.version_compare(self.version, '>=4.9.0'): @@ -383,3 +386,13 @@ class GnuCompiler(GnuLikeCompiler): def get_prelink_args(self, prelink_name: str, obj_list: T.List[str]) -> T.List[str]: return ['-r', '-o', prelink_name] + obj_list + + def get_lto_compile_args(self, *, threads: int = 0, mode: str = 'default') -> T.List[str]: + if threads == 0: + if mesonlib.version_compare(self.version, '>= 10.0'): + return ['-flto=auto'] + # This matches clang's behavior of using the number of cpus + return [f'-flto={multiprocessing.cpu_count()}'] + elif threads > 0: + return [f'-flto={threads}'] + return super().get_lto_compile_args(threads=threads) diff --git a/mesonbuild/dependencies/cuda.py b/mesonbuild/dependencies/cuda.py index c04e2fc..20f6569 100644 --- a/mesonbuild/dependencies/cuda.py +++ b/mesonbuild/dependencies/cuda.py @@ -219,7 +219,7 @@ class CudaDependency(ExternalDependency): raise DependencyException(msg.format(arch, 'Windows')) return os.path.join('lib', libdirs[arch]) elif machine.is_linux(): - libdirs = {'x86_64': 'lib64', 'ppc64': 'lib', 'aarch64': 'lib64'} + libdirs = {'x86_64': 'lib64', 'ppc64': 'lib', 'aarch64': 'lib64', 'loongarch64': 'lib64'} if arch not in libdirs: raise DependencyException(msg.format(arch, 'Linux')) return libdirs[arch] diff --git a/mesonbuild/envconfig.py b/mesonbuild/envconfig.py index 6713135..ba35d16 100644 --- a/mesonbuild/envconfig.py +++ b/mesonbuild/envconfig.py @@ -45,6 +45,7 @@ known_cpu_families = ( 'dspic', 'e2k', 'ia64', + 'loongarch64', 'm68k', 'microblaze', 'mips', @@ -74,6 +75,7 @@ CPU_FAMILIES_64_BIT = [ 'aarch64', 'alpha', 'ia64', + 'loongarch64', 'mips64', 'ppc64', 'riscv64', diff --git a/mesonbuild/environment.py b/mesonbuild/environment.py index 219b0f2..b12728b 100644 --- a/mesonbuild/environment.py +++ b/mesonbuild/environment.py @@ -1006,7 +1006,7 @@ class Environment: def _handle_exceptions(self, exceptions, binaries, bintype='compiler'): errmsg = 'Unknown {}(s): {}'.format(bintype, binaries) if exceptions: - errmsg += '\nThe follow exceptions were encountered:' + errmsg += '\nThe following exception(s) were encountered:' for (c, e) in exceptions.items(): errmsg += '\nRunning "{0}" gave "{1}"'.format(c, e) raise EnvironmentException(errmsg) @@ -1778,8 +1778,8 @@ class Environment: mlog.warning( 'Please do not put -C linker= in your compiler ' 'command, set rust_ld=command in your cross file ' - 'or use the RUST_LD environment variable. meson ' - 'will override your seletion otherwise.') + 'or use the RUST_LD environment variable, otherwise meson ' + 'will override your selection.') if override is None: extra_args = {} @@ -1818,7 +1818,7 @@ class Environment: else: # On linux and macos rust will invoke the c compiler for # linking, on windows it will use lld-link or link.exe. - # we will simply ask for the C compiler that coresponds to + # we will simply ask for the C compiler that corresponds to # it, and use that. cc = self._detect_c_or_cpp_compiler('c', for_machine, override_compiler=override) linker = cc.linker diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py index 586e94e..176c1da 100644 --- a/mesonbuild/interpreter.py +++ b/mesonbuild/interpreter.py @@ -409,6 +409,7 @@ class ConfigurationDataHolder(MutableInterpreterObject, ObjectHolder): return self.held_object.values[name] # (val, desc) @FeatureNew('configuration_data.keys()', '0.57.0') + @noPosargs def keys_method(self, args, kwargs): return sorted(self.keys()) @@ -2378,7 +2379,7 @@ permitted_kwargs = {'add_global_arguments': {'language', 'native'}, 'jar': build.known_jar_kwargs, 'project': {'version', 'meson_version', 'default_options', 'license', 'subproject_dir'}, 'run_command': {'check', 'capture', 'env'}, - 'run_target': {'command', 'depends'}, + 'run_target': {'command', 'depends', 'env'}, 'shared_library': build.known_shlib_kwargs, 'shared_module': build.known_shmod_kwargs, 'static_library': build.known_stlib_kwargs, @@ -2618,7 +2619,7 @@ class Interpreter(InterpreterBase): def get_build_def_files(self) -> T.List[str]: return self.build_def_files - def add_build_def_file(self, f): + def add_build_def_file(self, f: mesonlib.FileOrString) -> None: # Use relative path for files within source directory, and absolute path # for system files. Skip files within build directory. Also skip not regular # files (e.g. /dev/stdout) Normalize the path to avoid duplicates, this @@ -3366,8 +3367,8 @@ external dependencies (including libraries) must go to "dependencies".''') raise InterpreterException('Problem encountered: ' + args[0]) @noKwargs + @noPosargs def func_exception(self, node, args, kwargs): - self.validate_arguments(args, 0, []) raise Exception() def add_languages(self, args: T.Sequence[str], required: bool, for_machine: MachineChoice) -> bool: @@ -3984,6 +3985,7 @@ external dependencies (including libraries) must go to "dependencies".''') @permittedKwargs(permitted_kwargs['vcs_tag']) @FeatureDeprecatedKwargs('custom_target', '0.47.0', ['build_always'], 'combine build_by_default and build_always_stale instead.') + @noPosargs def func_vcs_tag(self, node, args, kwargs): if 'input' not in kwargs or 'output' not in kwargs: raise InterpreterException('Keyword arguments input and output must exist') @@ -4024,12 +4026,9 @@ external dependencies (including libraries) must go to "dependencies".''') return self._func_custom_target_impl(node, [kwargs['output']], kwargs) @FeatureNew('subdir_done', '0.46.0') - @stringArgs + @noPosargs + @noKwargs def func_subdir_done(self, node, args, kwargs): - if len(kwargs) > 0: - raise InterpreterException('exit does not take named arguments') - if len(args) > 0: - raise InterpreterException('exit does not take any arguments') raise SubdirDoneRequest() @stringArgs @@ -4060,6 +4059,7 @@ This will become a hard error in the future.''' % kwargs['input'], location=self self.add_target(name, tg.held_object) return tg + @FeatureNewKwargs('run_target', '0.57.0', ['env']) @permittedKwargs(permitted_kwargs['run_target']) def func_run_target(self, node, args, kwargs): if len(args) > 1: @@ -4088,8 +4088,8 @@ This will become a hard error in the future.''' % kwargs['input'], location=self if not isinstance(d, (build.BuildTarget, build.CustomTarget)): raise InterpreterException('Depends items must be build targets.') cleaned_deps.append(d) - command, *cmd_args = cleaned_args - tg = RunTargetHolder(build.RunTarget(name, command, cmd_args, cleaned_deps, self.subdir, self.subproject), self) + env = self.unpack_env_kwarg(kwargs) + tg = RunTargetHolder(build.RunTarget(name, cleaned_args, cleaned_deps, self.subdir, self.subproject, env), self) self.add_target(name, tg.held_object) full_name = (self.subproject, name) assert(full_name not in self.build.run_target_names) @@ -4412,9 +4412,8 @@ This will become a hard error in the future.''' % kwargs['input'], location=self @FeatureNewKwargs('configure_file', '0.50.0', ['install']) @FeatureNewKwargs('configure_file', '0.52.0', ['depfile']) @permittedKwargs(permitted_kwargs['configure_file']) + @noPosargs def func_configure_file(self, node, args, kwargs): - if len(args) > 0: - raise InterpreterException("configure_file takes only keyword arguments.") if 'output' not in kwargs: raise InterpreterException('Required keyword argument "output" not defined.') actions = set(['configuration', 'command', 'copy']).intersection(kwargs.keys()) diff --git a/mesonbuild/interpreterbase.py b/mesonbuild/interpreterbase.py index f17dfba..e924e93 100644 --- a/mesonbuild/interpreterbase.py +++ b/mesonbuild/interpreterbase.py @@ -18,10 +18,11 @@ from . import mparser, mesonlib, mlog from . import environment, dependencies +from functools import wraps import abc -import os, copy, re import collections.abc -from functools import wraps +import itertools +import os, copy, re import typing as T TV_fw_var = T.Union[str, int, float, bool, list, dict, 'InterpreterObject', 'ObjectHolder'] @@ -228,6 +229,127 @@ class permittedKwargs: return f(*wrapped_args, **wrapped_kwargs) return T.cast(TV_func, wrapped) + +def typed_pos_args(name: str, *types: T.Union[T.Type, T.Tuple[T.Type, ...]], + varargs: T.Optional[T.Union[T.Type, T.Tuple[T.Type]]] = None, + optargs: T.Optional[T.List[T.Union[T.Type, T.Tuple[T.Type]]]] = None, + min_varargs: int = 0, max_varargs: int = 0) -> T.Callable[..., T.Any]: + """Decorator that types type checking of positional arguments. + + This supports two different models of optional aguments, the first is the + variadic argument model. Variadic arguments are a possibly bounded, + possibly unbounded number of arguments of the same type (unions are + supported). The second is the standard default value model, in this case + a number of optional arguments may be provided, but they are still + ordered, and they may have different types. + + This function does not support mixing variadic and default arguments. + + :name: The name of the decorated function (as displayed in error messages) + :varargs: They type(s) of any variadic arguments the function takes. If + None the function takes no variadic args + :min_varargs: the minimum number of variadic arguments taken + :max_varargs: the maximum number of variadic arguments taken. 0 means unlimited + :optargs: The types of any optional arguments parameters taken. If None + then no optional paramters are taken. + + Some examples of usage blow: + >>> @typed_pos_args('mod.func', str, (str, int)) + ... def func(self, state: ModuleState, args: T.Tuple[str, T.Union[str, int]], kwargs: T.Dict[str, T.Any]) -> T.Any: + ... pass + + >>> @typed_pos_args('method', str, varargs=str) + ... def method(self, node: BaseNode, args: T.Tuple[str, T.List[str]], kwargs: T.Dict[str, T.Any]) -> T.Any: + ... pass + + >>> @typed_pos_args('method', varargs=str, min_varargs=1) + ... def method(self, node: BaseNode, args: T.Tuple[T.List[str]], kwargs: T.Dict[str, T.Any]) -> T.Any: + ... pass + + >>> @typed_pos_args('method', str, optargs=[(str, int), str]) + ... def method(self, node: BaseNode, args: T.Tuple[str, T.Optional[T.Union[str, int]], T.Optional[str]], kwargs: T.Dict[str, T.Any]) -> T.Any: + ... pass + + When should you chose `typed_pos_args('name', varargs=str, + min_varargs=1)` vs `typed_pos_args('name', str, varargs=str)`? + + The answer has to do with the semantics of the function, if all of the + inputs are the same type (such as with `files()`) then the former is + correct, all of the arguments are string names of files. If the first + argument is something else the it should be separated. + """ + def inner(f: TV_func) -> TV_func: + + @wraps(f) + def wrapper(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any: + args = _get_callee_args(wrapped_args)[2] + + # These are implementation programming errors, end users should never see them. + assert isinstance(args, list), args + assert max_varargs >= 0, 'max_varrags cannot be negative' + assert min_varargs >= 0, 'min_varrags cannot be negative' + assert optargs is None or varargs is None, \ + 'varargs and optargs not supported together as this would be ambiguous' + + num_args = len(args) + num_types = len(types) + a_types = types + + if varargs: + min_args = num_types + min_varargs + max_args = num_types + max_varargs + if max_varargs == 0 and num_args < min_args: + raise InvalidArguments(f'{name} takes at least {min_args} arguments, but got {num_args}.') + elif max_varargs != 0 and (num_args < min_args or num_args > max_args): + raise InvalidArguments(f'{name} takes between {min_args} and {max_args} arguments, but got {num_args}.') + elif optargs: + if num_args < num_types: + raise InvalidArguments(f'{name} takes at least {num_types} arguments, but got {num_args}.') + elif num_args > num_types + len(optargs): + raise InvalidArguments(f'{name} takes at most {num_types + len(optargs)} arguments, but got {num_args}.') + # Add the number of positional arguments required + if num_args > num_types: + diff = num_args - num_types + a_types = tuple(list(types) + list(optargs[:diff])) + elif num_args != num_types: + raise InvalidArguments(f'{name} takes exactly {num_types} arguments, but got {num_args}.') + + for i, (arg, type_) in enumerate(itertools.zip_longest(args, a_types, fillvalue=varargs), start=1): + if not isinstance(arg, type_): + if isinstance(type_, tuple): + shouldbe = 'one of: {}'.format(", ".join(f'"{t.__name__}"' for t in type_)) + else: + shouldbe = f'"{type_.__name__}"' + raise InvalidArguments(f'{name} argument {i} was of type "{type(arg).__name__}" but should have been {shouldbe}') + + # Ensure that we're actually passing a tuple. + # Depending on what kind of function we're calling the length of + # wrapped_args can vary. + nargs = list(wrapped_args) + i = nargs.index(args) + if varargs: + # if we have varargs we need to split them into a separate + # tuple, as python's typing doesn't understand tuples with + # fixed elements and variadic elements, only one or the other. + # so in that case we need T.Tuple[int, str, float, T.Tuple[str, ...]] + pos = args[:len(types)] + var = list(args[len(types):]) + pos.append(var) + nargs[i] = tuple(pos) + elif optargs: + if num_args < num_types + len(optargs): + diff = num_types + len(optargs) - num_args + nargs[i] = tuple(list(args) + [None] * diff) + else: + nargs[i] = args + else: + nargs[i] = tuple(args) + return f(*nargs, **wrapped_kwargs) + + return T.cast(TV_func, wrapper) + return inner + + class FeatureCheckBase(metaclass=abc.ABCMeta): "Base class for feature version checks" diff --git a/mesonbuild/minstall.py b/mesonbuild/minstall.py index 18dca8b..6c1810a 100644 --- a/mesonbuild/minstall.py +++ b/mesonbuild/minstall.py @@ -54,6 +54,7 @@ if T.TYPE_CHECKING: quiet: bool wd: str destdir: str + dry_run: bool symlink_warning = '''Warning: trying to copy a symlink that points to a file. This will copy the file, @@ -75,20 +76,27 @@ def add_arguments(parser: argparse.Namespace) -> None: help='Do not print every file that was installed.') parser.add_argument('--destdir', default=None, help='Sets or overrides DESTDIR environment. (Since 0.57.0)') + parser.add_argument('--dry-run', '-n', action='store_true', + help='Doesn\'t actually install, but print logs.') class DirMaker: - def __init__(self, lf: T.TextIO): + def __init__(self, lf: T.TextIO, makedirs: T.Callable[..., None]): self.lf = lf self.dirs: T.List[str] = [] + self.makedirs_impl = makedirs def makedirs(self, path: str, exist_ok: bool = False) -> None: dirname = os.path.normpath(path) dirs = [] while dirname != os.path.dirname(dirname): + if dirname in self.dirs: + # In dry-run mode the directory does not exist but we would have + # created it with all its parents otherwise. + break if not os.path.exists(dirname): dirs.append(dirname) dirname = os.path.dirname(dirname) - os.makedirs(path, exist_ok=exist_ok) + self.makedirs_impl(path, exist_ok=exist_ok) # store the directories in creation order, with the parent directory # before the child directories. Future calls of makedir() will not @@ -275,6 +283,74 @@ class Installer: self.options = options self.lf = lf self.preserved_file_count = 0 + self.dry_run = options.dry_run + + def mkdir(self, *args: T.Any, **kwargs: T.Any) -> None: + if not self.dry_run: + os.mkdir(*args, **kwargs) + + def remove(self, *args: T.Any, **kwargs: T.Any) -> None: + if not self.dry_run: + os.remove(*args, **kwargs) + + def symlink(self, *args: T.Any, **kwargs: T.Any) -> None: + if not self.dry_run: + os.symlink(*args, **kwargs) + + def makedirs(self, *args: T.Any, **kwargs: T.Any) -> None: + if not self.dry_run: + os.makedirs(*args, **kwargs) + + def copy(self, *args: T.Any, **kwargs: T.Any) -> None: + if not self.dry_run: + shutil.copy(*args, **kwargs) + + def copy2(self, *args: T.Any, **kwargs: T.Any) -> None: + if not self.dry_run: + shutil.copy2(*args, **kwargs) + + def copyfile(self, *args: T.Any, **kwargs: T.Any) -> None: + if not self.dry_run: + shutil.copyfile(*args, **kwargs) + + def copystat(self, *args: T.Any, **kwargs: T.Any) -> None: + if not self.dry_run: + shutil.copystat(*args, **kwargs) + + def fix_rpath(self, *args: T.Any, **kwargs: T.Any) -> None: + if not self.dry_run: + depfixer.fix_rpath(*args, **kwargs) + + def set_chown(self, *args: T.Any, **kwargs: T.Any) -> None: + if not self.dry_run: + set_chown(*args, **kwargs) + + def set_chmod(self, *args: T.Any, **kwargs: T.Any) -> None: + if not self.dry_run: + set_chmod(*args, **kwargs) + + def sanitize_permissions(self, *args: T.Any, **kwargs: T.Any) -> None: + if not self.dry_run: + sanitize_permissions(*args, **kwargs) + + def set_mode(self, *args: T.Any, **kwargs: T.Any) -> None: + if not self.dry_run: + set_mode(*args, **kwargs) + + def restore_selinux_contexts(self) -> None: + if not self.dry_run: + restore_selinux_contexts() + + def Popen_safe(self, *args: T.Any, **kwargs: T.Any) -> T.Tuple[int, str, str]: + if not self.dry_run: + p, o, e = Popen_safe(*args, **kwargs) + return p.returncode, o, e + return 0, '', '' + + def run_exe(self, *args: T.Any, **kwargs: T.Any) -> int: + if not self.dry_run: + return run_exe(*args, **kwargs) + return 0 def log(self, msg: str) -> None: if not self.options.quiet: @@ -307,7 +383,7 @@ class Installer: append_to_log(self.lf, '# Preserving old file {}\n'.format(to_file)) self.preserved_file_count += 1 return False - os.remove(to_file) + self.remove(to_file) elif makedirs: # Unpack tuple dirmaker, outdir = makedirs @@ -317,14 +393,14 @@ class Installer: if os.path.islink(from_file): if not os.path.exists(from_file): # Dangling symlink. Replicate as is. - shutil.copy(from_file, outdir, follow_symlinks=False) + self.copy(from_file, outdir, follow_symlinks=False) else: # Remove this entire branch when changing the behaviour to duplicate # symlinks rather than copying what they point to. print(symlink_warning) - shutil.copy2(from_file, to_file) + self.copy2(from_file, to_file) else: - shutil.copy2(from_file, to_file) + self.copy2(from_file, to_file) selinux_updates.append(to_file) append_to_log(self.lf, to_file) return True @@ -378,8 +454,8 @@ class Installer: print('Tried to copy directory {} but a file of that name already exists.'.format(abs_dst)) sys.exit(1) dm.makedirs(abs_dst) - shutil.copystat(abs_src, abs_dst) - sanitize_permissions(abs_dst, data.install_umask) + self.copystat(abs_src, abs_dst) + self.sanitize_permissions(abs_dst, data.install_umask) for f in files: abs_src = os.path.join(root, f) filepart = os.path.relpath(abs_src, start=src_dir) @@ -391,11 +467,11 @@ class Installer: sys.exit(1) parent_dir = os.path.dirname(abs_dst) if not os.path.isdir(parent_dir): - os.mkdir(parent_dir) - shutil.copystat(os.path.dirname(abs_src), parent_dir) + self.mkdir(parent_dir) + self.copystat(os.path.dirname(abs_src), parent_dir) # FIXME: what about symlinks? self.do_copyfile(abs_src, abs_dst) - set_mode(abs_dst, install_mode, data.install_umask) + self.set_mode(abs_dst, install_mode, data.install_umask) @staticmethod def check_installdata(obj: InstallData) -> InstallData: @@ -422,13 +498,13 @@ class Installer: self.did_install_something = False try: - with DirMaker(self.lf) as dm: + with DirMaker(self.lf, self.makedirs) as dm: self.install_subdirs(d, dm, destdir, fullprefix) # Must be first, because it needs to delete the old subtree. self.install_targets(d, dm, destdir, fullprefix) self.install_headers(d, dm, destdir, fullprefix) self.install_man(d, dm, destdir, fullprefix) self.install_data(d, dm, destdir, fullprefix) - restore_selinux_contexts() + self.restore_selinux_contexts() self.run_install_script(d, destdir, fullprefix) if not self.did_install_something: self.log('Nothing to install.') @@ -460,7 +536,7 @@ class Installer: outdir = os.path.dirname(outfilename) if self.do_copyfile(fullfilename, outfilename, makedirs=(dm, outdir)): self.did_install_something = True - set_mode(outfilename, mode, d.install_umask) + self.set_mode(outfilename, mode, d.install_umask) def install_man(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix: str) -> None: for m in d.man: @@ -470,7 +546,7 @@ class Installer: install_mode = m[2] if self.do_copyfile(full_source_filename, outfilename, makedirs=(dm, outdir)): self.did_install_something = True - set_mode(outfilename, install_mode, d.install_umask) + self.set_mode(outfilename, install_mode, d.install_umask) def install_headers(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix: str) -> None: for t in d.headers: @@ -481,7 +557,7 @@ class Installer: install_mode = t[2] if self.do_copyfile(fullfilename, outfilename, makedirs=(dm, outdir)): self.did_install_something = True - set_mode(outfilename, install_mode, d.install_umask) + self.set_mode(outfilename, install_mode, d.install_umask) def run_install_script(self, d: InstallData, destdir: str, fullprefix: str) -> None: env = {'MESON_SOURCE_ROOT': d.source_dir, @@ -501,7 +577,7 @@ class Installer: self.did_install_something = True # Custom script must report itself if it does nothing. self.log('Running custom install script {!r}'.format(name)) try: - rc = run_exe(i, env) + rc = self.run_exe(i, env) except OSError: print('FAILED: install script \'{}\' could not be run, stopped'.format(name)) # POSIX shells return 127 when a command could not be found @@ -533,14 +609,14 @@ class Installer: raise RuntimeError('File {!r} could not be found'.format(fname)) elif os.path.isfile(fname): file_copied = self.do_copyfile(fname, outname, makedirs=(dm, outdir)) - set_mode(outname, install_mode, d.install_umask) + self.set_mode(outname, install_mode, d.install_umask) if should_strip and d.strip_bin is not None: if fname.endswith('.jar'): self.log('Not stripping jar target: {}'.format(os.path.basename(fname))) continue self.log('Stripping target {!r} using {}.'.format(fname, d.strip_bin[0])) - ps, stdo, stde = Popen_safe(d.strip_bin + [outname]) - if ps.returncode != 0: + returncode, stdo, stde = self.Popen_safe(d.strip_bin + [outname]) + if returncode != 0: print('Could not strip file.\n') print('Stdout:\n{}\n'.format(stdo)) print('Stderr:\n{}\n'.format(stde)) @@ -564,10 +640,10 @@ class Installer: try: symlinkfilename = os.path.join(outdir, alias) try: - os.remove(symlinkfilename) + self.remove(symlinkfilename) except FileNotFoundError: pass - os.symlink(to, symlinkfilename) + self.symlink(to, symlinkfilename) append_to_log(self.lf, symlinkfilename) except (NotImplementedError, OSError): if not printed_symlink_error: @@ -577,8 +653,8 @@ class Installer: if file_copied: self.did_install_something = True try: - depfixer.fix_rpath(outname, t.rpath_dirs_to_remove, install_rpath, final_path, - install_name_mappings, verbose=False) + self.fix_rpath(outname, t.rpath_dirs_to_remove, install_rpath, final_path, + install_name_mappings, verbose=False) except SystemExit as e: if isinstance(e.code, int) and e.code == 0: pass diff --git a/mesonbuild/modules/fs.py b/mesonbuild/modules/fs.py index 2ff256b..44986f8 100644 --- a/mesonbuild/modules/fs.py +++ b/mesonbuild/modules/fs.py @@ -14,18 +14,25 @@ import typing as T import hashlib +import os from pathlib import Path, PurePath, PureWindowsPath from .. import mlog from . import ExtensionModule from . import ModuleReturnValue -from ..mesonlib import MesonException +from ..mesonlib import ( + File, + FileOrString, + MesonException, + path_is_in_root, +) from ..interpreterbase import FeatureNew from ..interpreterbase import stringArgs, noKwargs if T.TYPE_CHECKING: from ..interpreter import Interpreter, ModuleState + class FSModule(ExtensionModule): def __init__(self, interpreter: 'Interpreter') -> None: @@ -193,5 +200,61 @@ class FSModule(ExtensionModule): new = original.stem return ModuleReturnValue(str(new), []) + #@permittedKwargs({'encoding'}) + @FeatureNew('fs.read', '0.57.0') + def read( + self, + state: 'ModuleState', + args: T.Sequence['FileOrString'], + kwargs: T.Dict[str, T.Any] + ) -> ModuleReturnValue: + ''' + Read a file from the source tree and return its value as a decoded + string. If the encoding is not specified, the file is assumed to be + utf-8 encoded. Paths must be relative by default (to prevent accidents) + and are forbidden to be read from the build directory (to prevent build + loops) + ''' + if len(args) != 1: + raise MesonException('expected single positional <path> arg') + + path = args[0] + if not isinstance(path, (str, File)): + raise MesonException( + '<path> positional argument must be string or files() object') + + encoding = kwargs.get('encoding', 'utf-8') + if not isinstance(encoding, str): + raise MesonException('`encoding` parameter must be a string') + + src_dir = self.interpreter.environment.source_dir + sub_dir = self.interpreter.subdir + build_dir = self.interpreter.environment.get_build_dir() + + if isinstance(path, File): + if path.is_built: + raise MesonException( + 'fs.read_file does not accept built files() objects') + path = os.path.join(src_dir, path.relative_name()) + else: + if sub_dir: + src_dir = os.path.join(src_dir, sub_dir) + path = os.path.join(src_dir, path) + + path = os.path.abspath(path) + if path_is_in_root(Path(path), Path(build_dir), resolve=True): + raise MesonException('path must not be in the build tree') + try: + with open(path, 'r', encoding=encoding) as f: + data = f.read() + except UnicodeDecodeError: + raise MesonException(f'decoding failed for {path}') + # Reconfigure when this file changes as it can contain data used by any + # part of the build configuration (e.g. `project(..., version: + # fs.read_file('VERSION')` or `configure_file(...)` + self.interpreter.add_build_def_file(path) + return ModuleReturnValue(data, []) + + def initialize(*args: T.Any, **kwargs: T.Any) -> FSModule: return FSModule(*args, **kwargs) diff --git a/mesonbuild/modules/gnome.py b/mesonbuild/modules/gnome.py index f564eb4..f966083 100644 --- a/mesonbuild/modules/gnome.py +++ b/mesonbuild/modules/gnome.py @@ -34,7 +34,7 @@ from ..mesonlib import ( join_args, unholder, ) from ..dependencies import Dependency, PkgConfigDependency, InternalDependency, ExternalProgram -from ..interpreterbase import noKwargs, permittedKwargs, FeatureNew, FeatureNewKwargs, FeatureDeprecatedKwargs +from ..interpreterbase import noPosargs, noKwargs, permittedKwargs, FeatureNew, FeatureNewKwargs, FeatureDeprecatedKwargs if T.TYPE_CHECKING: from ..compilers import Compiler @@ -51,6 +51,10 @@ native_glib_version = None class GnomeModule(ExtensionModule): gir_dep = None + install_glib_compile_schemas = False + install_gio_querymodules = [] + install_gtk_update_icon_cache = False + @staticmethod def _get_native_glib_version(state): global native_glib_version @@ -80,6 +84,65 @@ class GnomeModule(ExtensionModule): mlog.bold('https://github.com/mesonbuild/meson/issues/1387'), once=True) + def _get_native_dep(self, state, depname, required=True): + kwargs = {'native': True, 'required': required} + holder = self.interpreter.func_dependency(state.current_node, [depname], kwargs) + return holder.held_object + + def _get_native_binary(self, state, name, depname, varname, required=True): + # Look in overrides in case glib/gtk/etc are built as subproject + prog = self.interpreter.program_from_overrides([name], []) + if prog is not None: + return unholder(prog) + + # Look in machine file + prog = state.environment.lookup_binary_entry(MachineChoice.HOST, name) + if prog is not None: + return ExternalProgram.from_entry(name, prog) + + # Check if pkgconfig has a variable + dep = self._get_native_dep(state, depname, required=False) + if dep.found() and dep.type_name == 'pkgconfig': + value = dep.get_pkgconfig_variable(varname, {}) + if value: + return ExternalProgram(name, value) + + # Normal program lookup + return unholder(self.interpreter.find_program_impl(name, required=required)) + + @permittedKwargs({'glib_compile_schemas', 'gio_querymodules', 'gtk_update_icon_cache'}) + @noPosargs + @FeatureNew('gnome.post_install', '0.57.0') + def post_install(self, state, args, kwargs): + rv = [] + datadir_abs = os.path.join(state.environment.get_prefix(), state.environment.get_datadir()) + if kwargs.get('glib_compile_schemas', False) and not self.install_glib_compile_schemas: + self.install_glib_compile_schemas = True + prog = self._get_native_binary(state, 'glib-compile-schemas', 'gio-2.0', 'glib_compile_schemas') + schemasdir = os.path.join(datadir_abs, 'glib-2.0', 'schemas') + script = state.backend.get_executable_serialisation([prog, schemasdir]) + script.skip_if_destdir = True + rv.append(script) + for d in mesonlib.extract_as_list(kwargs, 'gio_querymodules'): + if d not in self.install_gio_querymodules: + self.install_gio_querymodules.append(d) + prog = self._get_native_binary(state, 'gio-querymodules', 'gio-2.0', 'gio_querymodules') + moduledir = os.path.join(state.environment.get_prefix(), d) + script = state.backend.get_executable_serialisation([prog, moduledir]) + script.skip_if_destdir = True + rv.append(script) + if kwargs.get('gtk_update_icon_cache', False) and not self.install_gtk_update_icon_cache: + self.install_gtk_update_icon_cache = True + prog = self._get_native_binary(state, 'gtk4-update-icon-cache', 'gtk-4.0', 'gtk4_update_icon_cache', required=False) + found = isinstance(prog, build.Executable) or prog.found() + if not found: + prog = self._get_native_binary(state, 'gtk-update-icon-cache', 'gtk+-3.0', 'gtk_update_icon_cache') + icondir = os.path.join(datadir_abs, 'icons', 'hicolor') + script = state.backend.get_executable_serialisation([prog, '-q', '-t' ,'-f', icondir]) + script.skip_if_destdir = True + rv.append(script) + return ModuleReturnValue(None, rv) + @FeatureNewKwargs('gnome.compile_resources', '0.37.0', ['gresource_bundle', 'export', 'install_header']) @permittedKwargs({'source_dir', 'c_name', 'dependencies', 'export', 'gresource_bundle', 'install_header', 'install', 'install_dir', 'extra_args', 'build_by_default'}) @@ -418,23 +481,9 @@ class GnomeModule(ExtensionModule): def _get_gir_dep(self, state): if not self.gir_dep: - kwargs = {'native': True, 'required': True} - holder = self.interpreter.func_dependency(state.current_node, ['gobject-introspection-1.0'], kwargs) - self.gir_dep = holder.held_object - giscanner = state.environment.lookup_binary_entry(MachineChoice.HOST, 'g-ir-scanner') - if giscanner is not None: - self.giscanner = ExternalProgram.from_entry('g-ir-scanner', giscanner) - elif self.gir_dep.type_name == 'pkgconfig': - self.giscanner = ExternalProgram('g_ir_scanner', self.gir_dep.get_pkgconfig_variable('g_ir_scanner', {})) - else: - self.giscanner = self.interpreter.find_program_impl('g-ir-scanner') - gicompiler = state.environment.lookup_binary_entry(MachineChoice.HOST, 'g-ir-compiler') - if gicompiler is not None: - self.gicompiler = ExternalProgram.from_entry('g-ir-compiler', gicompiler) - elif self.gir_dep.type_name == 'pkgconfig': - self.gicompiler = ExternalProgram('g_ir_compiler', self.gir_dep.get_pkgconfig_variable('g_ir_compiler', {})) - else: - self.gicompiler = self.interpreter.find_program_impl('g-ir-compiler') + self.gir_dep = self._get_native_dep(state, 'gobject-introspection-1.0') + self.giscanner = self._get_native_binary(state, 'g-ir-scanner', 'gobject-introspection-1.0', 'g_ir_scanner') + self.gicompiler = self._get_native_binary(state, 'g-ir-compiler', 'gobject-introspection-1.0', 'g_ir_compiler') return self.gir_dep, self.giscanner, self.gicompiler @functools.lru_cache(maxsize=None) @@ -907,8 +956,8 @@ class GnomeModule(ExtensionModule): '--id=' + project_id, '--sources=' + source_str, ] - pottarget = build.RunTarget('help-' + project_id + '-pot', potargs[0], - potargs[1:], [], state.subdir, state.subproject) + pottarget = build.RunTarget('help-' + project_id + '-pot', potargs, + [], state.subdir, state.subproject) poargs = state.environment.get_build_command() + [ '--internal', 'yelphelper', 'update-po', @@ -917,8 +966,8 @@ class GnomeModule(ExtensionModule): '--sources=' + source_str, '--langs=' + '@@'.join(langs), ] - potarget = build.RunTarget('help-' + project_id + '-update-po', poargs[0], - poargs[1:], [], state.subdir, state.subproject) + potarget = build.RunTarget('help-' + project_id + '-update-po', poargs, + [], state.subdir, state.subproject) rv = [inscript, pottarget, potarget] return ModuleReturnValue(None, rv) diff --git a/mesonbuild/modules/i18n.py b/mesonbuild/modules/i18n.py index ae24e6e..54faf4c 100644 --- a/mesonbuild/modules/i18n.py +++ b/mesonbuild/modules/i18n.py @@ -152,12 +152,12 @@ class I18nModule(ExtensionModule): potargs.append(datadirs) if extra_args: potargs.append(extra_args) - pottarget = build.RunTarget(packagename + '-pot', potargs[0], potargs[1:], [], state.subdir, state.subproject) + pottarget = build.RunTarget(packagename + '-pot', potargs, [], state.subdir, state.subproject) gmoargs = state.environment.get_build_command() + ['--internal', 'gettext', 'gen_gmo'] if lang_arg: gmoargs.append(lang_arg) - gmotarget = build.RunTarget(packagename + '-gmo', gmoargs[0], gmoargs[1:], [], state.subdir, state.subproject) + gmotarget = build.RunTarget(packagename + '-gmo', gmoargs, [], state.subdir, state.subproject) updatepoargs = state.environment.get_build_command() + ['--internal', 'gettext', 'update_po', pkg_arg] if lang_arg: @@ -166,7 +166,7 @@ class I18nModule(ExtensionModule): updatepoargs.append(datadirs) if extra_args: updatepoargs.append(extra_args) - updatepotarget = build.RunTarget(packagename + '-update-po', updatepoargs[0], updatepoargs[1:], [], state.subdir, state.subproject) + updatepotarget = build.RunTarget(packagename + '-update-po', updatepoargs, [], state.subdir, state.subproject) targets = [pottarget, gmotarget, updatepotarget] diff --git a/mesonbuild/modules/unstable_cuda.py b/mesonbuild/modules/unstable_cuda.py index 0f9d681..0a5f031 100644 --- a/mesonbuild/modules/unstable_cuda.py +++ b/mesonbuild/modules/unstable_cuda.py @@ -43,6 +43,8 @@ class CudaModule(ExtensionModule): cuda_version = args[0] driver_version_table = [ + {'cuda_version': '>=11.2.0', 'windows': '460.89', 'linux': '460.27.04'}, + {'cuda_version': '>=11.1.1', 'windows': '456.81', 'linux': '455.32'}, {'cuda_version': '>=11.1.0', 'windows': '456.38', 'linux': '455.23'}, {'cuda_version': '>=11.0.3', 'windows': '451.82', 'linux': '450.51.06'}, {'cuda_version': '>=11.0.2', 'windows': '451.48', 'linux': '450.51.05'}, diff --git a/mesonbuild/modules/unstable_rust.py b/mesonbuild/modules/unstable_rust.py index d215376..e74c181 100644 --- a/mesonbuild/modules/unstable_rust.py +++ b/mesonbuild/modules/unstable_rust.py @@ -18,8 +18,8 @@ from . import ExtensionModule, ModuleReturnValue from .. import mlog from ..build import BuildTarget, Executable, InvalidArguments from ..dependencies import Dependency, ExternalLibrary -from ..interpreter import ExecutableHolder, permitted_kwargs -from ..interpreterbase import InterpreterException, permittedKwargs, FeatureNew +from ..interpreter import ExecutableHolder, BuildTargetHolder, permitted_kwargs +from ..interpreterbase import InterpreterException, permittedKwargs, FeatureNew, typed_pos_args from ..mesonlib import stringlistify, unholder, listify if T.TYPE_CHECKING: @@ -35,7 +35,8 @@ class RustModule(ExtensionModule): super().__init__(interpreter) @permittedKwargs(permitted_kwargs['test'] | {'dependencies'} ^ {'protocol'}) - def test(self, state: 'ModuleState', args: T.List, kwargs: T.Dict[str, T.Any]) -> ModuleReturnValue: + @typed_pos_args('rust.test', str, BuildTargetHolder) + def test(self, state: 'ModuleState', args: T.Tuple[str, BuildTargetHolder], kwargs: T.Dict[str, T.Any]) -> ModuleReturnValue: """Generate a rust test target from a given rust target. Rust puts it's unitests inside it's main source files, unlike most @@ -77,14 +78,8 @@ class RustModule(ExtensionModule): rust.test('rust_lib_test', rust_lib) ``` """ - if len(args) != 2: - raise InterpreterException('rustmod.test() takes exactly 2 positional arguments') - name: str = args[0] - if not isinstance(name, str): - raise InterpreterException('First positional argument to rustmod.test() must be a string') + name = args[0] base_target: BuildTarget = unholder(args[1]) - if not isinstance(base_target, BuildTarget): - raise InterpreterException('Second positional argument to rustmod.test() must be a library or executable') if not base_target.uses_rust(): raise InterpreterException('Second positional argument to rustmod.test() must be a rust based target') extra_args = stringlistify(kwargs.get('args', [])) diff --git a/mesonbuild/scripts/commandrunner.py b/mesonbuild/scripts/commandrunner.py deleted file mode 100644 index aeeaa3b..0000000 --- a/mesonbuild/scripts/commandrunner.py +++ /dev/null @@ -1,84 +0,0 @@ -# Copyright 2014 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. - -"""This program is a wrapper to run external commands. It determines -what to run, sets up the environment and executes the command.""" - -import sys, os, subprocess, shutil, shlex -import re -import typing as T - -def run_command(source_dir: str, build_dir: str, subdir: str, meson_command: T.List[str], command: str, arguments: T.List[str]) -> subprocess.Popen: - env = {'MESON_SOURCE_ROOT': source_dir, - 'MESON_BUILD_ROOT': build_dir, - 'MESON_SUBDIR': subdir, - 'MESONINTROSPECT': ' '.join([shlex.quote(x) for x in meson_command + ['introspect']]), - } - cwd = os.path.join(source_dir, subdir) - child_env = os.environ.copy() - child_env.update(env) - - # Is the command an executable in path? - exe = shutil.which(command) - if exe is not None: - command_array = [exe] + arguments - else:# No? Maybe it is a script in the source tree. - fullpath = os.path.join(source_dir, subdir, command) - command_array = [fullpath] + arguments - try: - return subprocess.Popen(command_array, env=child_env, cwd=cwd) - except FileNotFoundError: - print('Could not execute command "%s". File not found.' % command) - sys.exit(1) - except PermissionError: - print('Could not execute command "%s". File not executable.' % command) - sys.exit(1) - except OSError as err: - print('Could not execute command "{}": {}'.format(command, err)) - sys.exit(1) - except subprocess.SubprocessError as err: - print('Could not execute command "{}": {}'.format(command, err)) - sys.exit(1) - -def is_python_command(cmdname: str) -> bool: - end_py_regex = r'python(3|3\.\d+)?(\.exe)?$' - return re.search(end_py_regex, cmdname) is not None - -def run(args: T.List[str]) -> int: - if len(args) < 4: - print('commandrunner.py <source dir> <build dir> <subdir> <command> [arguments]') - return 1 - src_dir = args[0] - build_dir = args[1] - subdir = args[2] - meson_bin = args[3] - if is_python_command(meson_bin): - meson_command = [meson_bin, args[4]] - command = args[5] - arguments = args[6:] - else: - meson_command = [meson_bin] - command = args[4] - arguments = args[5:] - pc = run_command(src_dir, build_dir, subdir, meson_command, command, arguments) - while True: - try: - pc.wait() - break - except KeyboardInterrupt: - pass - return pc.returncode - -if __name__ == '__main__': - sys.exit(run(sys.argv[1:])) diff --git a/mesonbuild/scripts/meson_exe.py b/mesonbuild/scripts/meson_exe.py index 620f579..27db144 100644 --- a/mesonbuild/scripts/meson_exe.py +++ b/mesonbuild/scripts/meson_exe.py @@ -52,10 +52,13 @@ def run_exe(exe: ExecutableSerialisation, extra_env: T.Optional[dict] = None) -> ['Z:' + p for p in exe.extra_paths] + child_env.get('WINEPATH', '').split(';') ) + pipe = subprocess.PIPE + if exe.verbose: + assert not exe.capture, 'Cannot capture and print to console at the same time' + pipe = None + p = subprocess.Popen(cmd_args, env=child_env, cwd=exe.workdir, - close_fds=False, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + close_fds=False, stdout=pipe, stderr=pipe) stdout, stderr = p.communicate() if p.returncode == 0xc0000135: @@ -65,6 +68,8 @@ def run_exe(exe: ExecutableSerialisation, extra_env: T.Optional[dict] = None) -> if p.returncode != 0: if exe.pickled: print('while executing {!r}'.format(cmd_args)) + if exe.verbose: + return p.returncode if not exe.capture: print('--- stdout ---') print(stdout.decode()) diff --git a/run_unittests.py b/run_unittests.py index ebe8f48..2e7fe91 100755 --- a/run_unittests.py +++ b/run_unittests.py @@ -51,12 +51,13 @@ import mesonbuild.mesonlib import mesonbuild.coredata import mesonbuild.modules.gnome from mesonbuild.interpreter import Interpreter, ObjectHolder +from mesonbuild.interpreterbase import typed_pos_args, InvalidArguments from mesonbuild.ast import AstInterpreter from mesonbuild.mesonlib import ( BuildDirLock, LibType, MachineChoice, PerMachine, Version, is_windows, is_osx, is_cygwin, is_dragonflybsd, is_openbsd, is_haiku, is_sunos, windows_proof_rmtree, python_command, version_compare, split_args, - quote_arg, relpath, is_linux, git, GIT + quote_arg, relpath, is_linux, git ) from mesonbuild.environment import detect_ninja from mesonbuild.mesonlib import MesonException, EnvironmentException, OptionKey @@ -336,7 +337,6 @@ class InternalTests(unittest.TestCase): self.assertEqual(searchfunc('2016.oops 1.2.3'), '1.2.3') self.assertEqual(searchfunc('2016.x'), 'unknown version') - def test_mode_symbolic_to_bits(self): modefunc = mesonbuild.mesonlib.FileMode.perms_s_to_bits self.assertEqual(modefunc('---------'), 0) @@ -1294,6 +1294,195 @@ class InternalTests(unittest.TestCase): self.assertFalse(errors) + def test_typed_pos_args_types(self) -> None: + @typed_pos_args('foo', str, int, bool) + def _(obj, node, args: T.Tuple[str, int, bool], kwargs) -> None: + self.assertIsInstance(args, tuple) + self.assertIsInstance(args[0], str) + self.assertIsInstance(args[1], int) + self.assertIsInstance(args[2], bool) + + _(None, mock.Mock(), ['string', 1, False], None) + + def test_typed_pos_args_types_invalid(self) -> None: + @typed_pos_args('foo', str, int, bool) + def _(obj, node, args: T.Tuple[str, int, bool], kwargs) -> None: + self.assertTrue(False) # should not be reachable + + with self.assertRaises(InvalidArguments) as cm: + _(None, mock.Mock(), ['string', 1.0, False], None) + self.assertEqual(str(cm.exception), 'foo argument 2 was of type "float" but should have been "int"') + + def test_typed_pos_args_types_wrong_number(self) -> None: + @typed_pos_args('foo', str, int, bool) + def _(obj, node, args: T.Tuple[str, int, bool], kwargs) -> None: + self.assertTrue(False) # should not be reachable + + with self.assertRaises(InvalidArguments) as cm: + _(None, mock.Mock(), ['string', 1], None) + self.assertEqual(str(cm.exception), 'foo takes exactly 3 arguments, but got 2.') + + with self.assertRaises(InvalidArguments) as cm: + _(None, mock.Mock(), ['string', 1, True, True], None) + self.assertEqual(str(cm.exception), 'foo takes exactly 3 arguments, but got 4.') + + def test_typed_pos_args_varargs(self) -> None: + @typed_pos_args('foo', str, varargs=str) + def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None: + self.assertIsInstance(args, tuple) + self.assertIsInstance(args[0], str) + self.assertIsInstance(args[1], list) + self.assertIsInstance(args[1][0], str) + self.assertIsInstance(args[1][1], str) + + _(None, mock.Mock(), ['string', 'var', 'args'], None) + + def test_typed_pos_args_varargs_not_given(self) -> None: + @typed_pos_args('foo', str, varargs=str) + def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None: + self.assertIsInstance(args, tuple) + self.assertIsInstance(args[0], str) + self.assertIsInstance(args[1], list) + self.assertEqual(args[1], []) + + _(None, mock.Mock(), ['string'], None) + + def test_typed_pos_args_varargs_invalid(self) -> None: + @typed_pos_args('foo', str, varargs=str) + def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None: + self.assertTrue(False) # should not be reachable + + with self.assertRaises(InvalidArguments) as cm: + _(None, mock.Mock(), ['string', 'var', 'args', 0], None) + self.assertEqual(str(cm.exception), 'foo argument 4 was of type "int" but should have been "str"') + + def test_typed_pos_args_varargs_invalid_mulitple_types(self) -> None: + @typed_pos_args('foo', str, varargs=(str, list)) + def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None: + self.assertTrue(False) # should not be reachable + + with self.assertRaises(InvalidArguments) as cm: + _(None, mock.Mock(), ['string', 'var', 'args', 0], None) + self.assertEqual(str(cm.exception), 'foo argument 4 was of type "int" but should have been one of: "str", "list"') + + def test_typed_pos_args_max_varargs(self) -> None: + @typed_pos_args('foo', str, varargs=str, max_varargs=5) + def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None: + self.assertIsInstance(args, tuple) + self.assertIsInstance(args[0], str) + self.assertIsInstance(args[1], list) + self.assertIsInstance(args[1][0], str) + self.assertIsInstance(args[1][1], str) + + _(None, mock.Mock(), ['string', 'var', 'args'], None) + + def test_typed_pos_args_max_varargs_exceeded(self) -> None: + @typed_pos_args('foo', str, varargs=str, max_varargs=1) + def _(obj, node, args: T.Tuple[str, T.Tuple[str, ...]], kwargs) -> None: + self.assertTrue(False) # should not be reachable + + with self.assertRaises(InvalidArguments) as cm: + _(None, mock.Mock(), ['string', 'var', 'args'], None) + self.assertEqual(str(cm.exception), 'foo takes between 1 and 2 arguments, but got 3.') + + def test_typed_pos_args_min_varargs(self) -> None: + @typed_pos_args('foo', varargs=str, max_varargs=2, min_varargs=1) + def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None: + self.assertIsInstance(args, tuple) + self.assertIsInstance(args[0], list) + self.assertIsInstance(args[0][0], str) + self.assertIsInstance(args[0][1], str) + + _(None, mock.Mock(), ['string', 'var'], None) + + def test_typed_pos_args_min_varargs_not_met(self) -> None: + @typed_pos_args('foo', str, varargs=str, min_varargs=1) + def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None: + self.assertTrue(False) # should not be reachable + + with self.assertRaises(InvalidArguments) as cm: + _(None, mock.Mock(), ['string'], None) + self.assertEqual(str(cm.exception), 'foo takes at least 2 arguments, but got 1.') + + def test_typed_pos_args_min_and_max_varargs_exceeded(self) -> None: + @typed_pos_args('foo', str, varargs=str, min_varargs=1, max_varargs=2) + def _(obj, node, args: T.Tuple[str, T.Tuple[str, ...]], kwargs) -> None: + self.assertTrue(False) # should not be reachable + + with self.assertRaises(InvalidArguments) as cm: + _(None, mock.Mock(), ['string', 'var', 'args', 'bar'], None) + self.assertEqual(str(cm.exception), 'foo takes between 2 and 3 arguments, but got 4.') + + def test_typed_pos_args_min_and_max_varargs_not_met(self) -> None: + @typed_pos_args('foo', str, varargs=str, min_varargs=1, max_varargs=2) + def _(obj, node, args: T.Tuple[str, T.Tuple[str, ...]], kwargs) -> None: + self.assertTrue(False) # should not be reachable + + with self.assertRaises(InvalidArguments) as cm: + _(None, mock.Mock(), ['string'], None) + self.assertEqual(str(cm.exception), 'foo takes between 2 and 3 arguments, but got 1.') + + def test_typed_pos_args_variadic_and_optional(self) -> None: + @typed_pos_args('foo', str, optargs=[str], varargs=str, min_varargs=0) + def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None: + self.assertTrue(False) # should not be reachable + + with self.assertRaises(AssertionError) as cm: + _(None, mock.Mock(), ['string'], None) + self.assertEqual( + str(cm.exception), + 'varargs and optargs not supported together as this would be ambiguous') + + def test_typed_pos_args_min_optargs_not_met(self) -> None: + @typed_pos_args('foo', str, str, optargs=[str]) + def _(obj, node, args: T.Tuple[str, T.Optional[str]], kwargs) -> None: + self.assertTrue(False) # should not be reachable + + with self.assertRaises(InvalidArguments) as cm: + _(None, mock.Mock(), ['string'], None) + self.assertEqual(str(cm.exception), 'foo takes at least 2 arguments, but got 1.') + + def test_typed_pos_args_min_optargs_max_exceeded(self) -> None: + @typed_pos_args('foo', str, optargs=[str]) + def _(obj, node, args: T.Tuple[str, T.Optional[str]], kwargs) -> None: + self.assertTrue(False) # should not be reachable + + with self.assertRaises(InvalidArguments) as cm: + _(None, mock.Mock(), ['string', '1', '2'], None) + self.assertEqual(str(cm.exception), 'foo takes at most 2 arguments, but got 3.') + + def test_typed_pos_args_optargs_not_given(self) -> None: + @typed_pos_args('foo', str, optargs=[str]) + def _(obj, node, args: T.Tuple[str, T.Optional[str]], kwargs) -> None: + self.assertEqual(len(args), 2) + self.assertIsInstance(args[0], str) + self.assertEqual(args[0], 'string') + self.assertIsNone(args[1]) + + _(None, mock.Mock(), ['string'], None) + + def test_typed_pos_args_optargs_some_given(self) -> None: + @typed_pos_args('foo', str, optargs=[str, int]) + def _(obj, node, args: T.Tuple[str, T.Optional[str], T.Optional[int]], kwargs) -> None: + self.assertEqual(len(args), 3) + self.assertIsInstance(args[0], str) + self.assertEqual(args[0], 'string') + self.assertIsInstance(args[1], str) + self.assertEqual(args[1], '1') + self.assertIsNone(args[2]) + + _(None, mock.Mock(), ['string', '1'], None) + + def test_typed_pos_args_optargs_all_given(self) -> None: + @typed_pos_args('foo', str, optargs=[str]) + def _(obj, node, args: T.Tuple[str, T.Optional[str]], kwargs) -> None: + self.assertEqual(len(args), 2) + self.assertIsInstance(args[0], str) + self.assertEqual(args[0], 'string') + self.assertIsInstance(args[1], str) + + _(None, mock.Mock(), ['string', '1'], None) + @unittest.skipIf(is_tarball(), 'Skipping because this is a tarball release') class DataTests(unittest.TestCase): @@ -1459,6 +1648,7 @@ class DataTests(unittest.TestCase): res = re.search(r'syn keyword mesonBuiltin(\s+\\\s\w+)+', f.read(), re.MULTILINE) defined = set([a.strip() for a in res.group().split('\\')][1:]) self.assertEqual(defined, set(chain(interp.funcs.keys(), interp.builtin.keys()))) + def test_all_functions_defined_in_ast_interpreter(self): ''' Ensure that the all functions defined in the Interpreter are also defined @@ -1490,7 +1680,6 @@ class DataTests(unittest.TestCase): for p in i.iterdir(): data_files += [(p.relative_to(mesonbuild_dir).as_posix(), hashlib.sha256(p.read_bytes()).hexdigest())] - from pprint import pprint current_files = set(mesondata.keys()) scanned_files = set([x[0] for x in data_files]) @@ -2192,6 +2381,7 @@ class AllPlatformTests(BasePlatformTests): testdir = os.path.join(self.common_test_dir, '52 run target') self.init(testdir) self.run_target('check_exists') + self.run_target('check-env') def test_install_introspection(self): ''' @@ -2270,11 +2460,13 @@ class AllPlatformTests(BasePlatformTests): expected = {installpath: 0} for name in installpath.rglob('*'): expected[name] = 0 - # Find logged files and directories - with Path(self.builddir, 'meson-logs', 'install-log.txt').open() as f: - logged = list(map(lambda l: Path(l.strip()), - filter(lambda l: not l.startswith('#'), - f.readlines()))) + def read_logs(): + # Find logged files and directories + with Path(self.builddir, 'meson-logs', 'install-log.txt').open() as f: + return list(map(lambda l: Path(l.strip()), + filter(lambda l: not l.startswith('#'), + f.readlines()))) + logged = read_logs() for name in logged: self.assertTrue(name in expected, 'Log contains extra entry {}'.format(name)) expected[name] += 1 @@ -2283,6 +2475,13 @@ class AllPlatformTests(BasePlatformTests): self.assertGreater(count, 0, 'Log is missing entry for {}'.format(name)) self.assertLess(count, 2, 'Log has multiple entries for {}'.format(name)) + # Verify that with --dry-run we obtain the same logs but with nothing + # actually installed + windows_proof_rmtree(self.installdir) + self._run(self.meson_command + ['install', '--dry-run', '--destdir', self.installdir], workdir=self.builddir) + self.assertEqual(logged, read_logs()) + self.assertFalse(os.path.exists(self.installdir)) + def test_uninstall(self): exename = os.path.join(self.installdir, 'usr/bin/prog' + exe_suffix) testdir = os.path.join(self.common_test_dir, '8 install') @@ -2763,7 +2962,7 @@ class AllPlatformTests(BasePlatformTests): for env_var in ['CPPFLAGS', 'CFLAGS']: env = {} env[env_var] = '-D{}="{}"'.format(define, value) - env['LDFLAGS'] = '-DMESON_FAIL_VALUE=cflags-read'.format(define) + env['LDFLAGS'] = '-DMESON_FAIL_VALUE=cflags-read' self.init(testdir, extra_args=['-D{}={}'.format(define, value)], override_envvars=env) def test_custom_target_exe_data_deterministic(self): @@ -2853,6 +3052,52 @@ class AllPlatformTests(BasePlatformTests): self.build() self.run_tests() + @skip_if_not_base_option('b_lto_threads') + def test_lto_threads(self): + testdir = os.path.join(self.common_test_dir, '6 linkshared') + + env = get_fake_env(testdir, self.builddir, self.prefix) + cc = env.detect_c_compiler(MachineChoice.HOST) + if cc.get_id() == 'clang' and is_windows(): + raise unittest.SkipTest('LTO not (yet) supported by windows clang') + + self.init(testdir, extra_args=['-Db_lto=true', '-Db_lto_threads=8']) + self.build() + self.run_tests() + + expected = set(cc.get_lto_compile_args(threads=8)) + targets = self.introspect('--targets') + # This assumes all of the targets support lto + for t in targets: + for s in t['target_sources']: + for e in expected: + self.assertIn(e, s['parameters']) + + @skip_if_not_base_option('b_lto_mode') + @skip_if_not_base_option('b_lto_threads') + def test_lto_mode(self): + testdir = os.path.join(self.common_test_dir, '6 linkshared') + + env = get_fake_env(testdir, self.builddir, self.prefix) + cc = env.detect_c_compiler(MachineChoice.HOST) + if cc.get_id() != 'clang': + raise unittest.SkipTest('Only clang currently supports thinLTO') + if cc.linker.id not in {'ld.lld', 'ld.gold', 'ld64', 'lld-link'}: + raise unittest.SkipTest('thinLTO requires ld.lld, ld.gold, ld64, or lld-link') + elif is_windows(): + raise unittest.SkipTest('LTO not (yet) supported by windows clang') + + self.init(testdir, extra_args=['-Db_lto=true', '-Db_lto_mode=thin', '-Db_lto_threads=8']) + self.build() + self.run_tests() + + expected = set(cc.get_lto_compile_args(threads=8, mode='thin')) + targets = self.introspect('--targets') + # This assumes all of the targets support lto + for t in targets: + for s in t['target_sources']: + assert expected.issubset(set(s['parameters'])), f'Incorrect values for {t["name"]}' + def test_dist_git(self): if not shutil.which('git'): raise unittest.SkipTest('Git not found') @@ -2882,7 +3127,6 @@ class AllPlatformTests(BasePlatformTests): except FileNotFoundError: return False - def test_dist_hg(self): if not self.has_working_hg(): raise unittest.SkipTest('Mercurial not found or broken.') @@ -6372,7 +6616,7 @@ class LinuxlikeTests(BasePlatformTests): elif compiler.language == 'cpp': env_flag_name = 'CXXFLAGS' else: - raise NotImplementedError('Language {} not defined.'.format(p)) + raise NotImplementedError('Language {} not defined.'.format(compiler.language)) env = {} env[env_flag_name] = cmd_std with self.assertRaises((subprocess.CalledProcessError, mesonbuild.mesonlib.EnvironmentException), @@ -7615,22 +7859,32 @@ class LinuxCrossMingwTests(BaseLinuxCrossTests): self.meson_cross_file = os.path.join(testdir, 'broken-cross.txt') # Force tracebacks so we can detect them properly env = {'MESON_FORCE_BACKTRACE': '1'} - with self.assertRaisesRegex(MesonException, 'exe_wrapper.*target.*use-exe-wrapper'): + error_message = "An exe_wrapper is needed but was not found. Please define one in cross file and check the command and/or add it to PATH." + error_message2 = "The exe_wrapper 'broken' defined in the cross file is needed by run target 'run-prog', but was not found. Please check the command and/or add it to PATH." + + with self.assertRaises(MesonException) as cm: # Must run in-process or we'll get a generic CalledProcessError self.init(testdir, extra_args='-Drun-target=false', inprocess=True, override_envvars=env) - with self.assertRaisesRegex(MesonException, 'exe_wrapper.*run target.*run-prog'): + self.assertEqual(str(cm.exception), error_message) + + with self.assertRaises(MesonException) as cm: # Must run in-process or we'll get a generic CalledProcessError self.init(testdir, extra_args='-Dcustom-target=false', inprocess=True, override_envvars=env) + self.assertEqual(str(cm.exception), error_message2) + self.init(testdir, extra_args=['-Dcustom-target=false', '-Drun-target=false'], override_envvars=env) self.build() - with self.assertRaisesRegex(MesonException, 'exe_wrapper.*PATH'): + + with self.assertRaises(MesonException) as cm: # Must run in-process or we'll get a generic CalledProcessError self.run_tests(inprocess=True, override_envvars=env) + self.assertEqual(str(cm.exception), + "The exe_wrapper defined in the cross file 'broken' was not found. Please check the command and/or add it to PATH.") @skipIfNoPkgconfig def test_cross_pkg_config_option(self): @@ -8577,22 +8831,6 @@ class NativeFileTests(BasePlatformTests): else: self.fail('Did not find bindir in build options?') - def test_builtin_options_paths_legacy(self): - testcase = os.path.join(self.common_test_dir, '1 trivial') - config = self.helper_create_native_file({ - 'built-in options': {'default_library': 'static'}, - 'paths': {'bindir': 'bar'}, - }) - - self.init(testcase, extra_args=['--native-file', config]) - configuration = self.introspect('--buildoptions') - for each in configuration: - if each['name'] == 'bindir': - self.assertEqual(each['value'], 'bar') - break - else: - self.fail('Did not find bindir in build options?') - class CrossFileTests(BasePlatformTests): diff --git a/test cases/cmake/15 object library advanced/meson.build b/test cases/cmake/15 object library advanced/meson.build index 6a4448b..4009a0d 100644 --- a/test cases/cmake/15 object library advanced/meson.build +++ b/test cases/cmake/15 object library advanced/meson.build @@ -1,5 +1,9 @@ project('cmake_object_lib_test', 'cpp', default_options: ['cpp_std=c++11']) +if meson.is_cross_build() + error('MESON_SKIP_TEST this test does not cross compile correctly.') +endif + cm = import('cmake') sub_pro = cm.subproject('cmObjLib') diff --git a/test cases/cmake/4 code gen/meson.build b/test cases/cmake/4 code gen/meson.build index 592f903..80c801f 100644 --- a/test cases/cmake/4 code gen/meson.build +++ b/test cases/cmake/4 code gen/meson.build @@ -1,5 +1,9 @@ project('cmake_code_gen', ['c', 'cpp']) +if meson.is_cross_build() + error('MESON_SKIP_TEST this test does not cross compile correctly.') +endif + cm = import('cmake') # Subproject with the "code generator" diff --git a/test cases/cmake/8 custom command/meson.build b/test cases/cmake/8 custom command/meson.build index 799e339..a262252 100644 --- a/test cases/cmake/8 custom command/meson.build +++ b/test cases/cmake/8 custom command/meson.build @@ -1,5 +1,9 @@ project('cmakeSubTest', ['c', 'cpp']) +if meson.is_cross_build() + error('MESON_SKIP_TEST this test does not cross compile correctly.') +endif + cm = import('cmake') sub_pro = cm.subproject('cmMod') diff --git a/test cases/common/241 get_file_contents/.gitattributes b/test cases/common/241 get_file_contents/.gitattributes new file mode 100644 index 0000000..abec47d --- /dev/null +++ b/test cases/common/241 get_file_contents/.gitattributes @@ -0,0 +1 @@ +utf-16-text binary diff --git a/test cases/common/241 get_file_contents/VERSION b/test cases/common/241 get_file_contents/VERSION new file mode 100644 index 0000000..26aaba0 --- /dev/null +++ b/test cases/common/241 get_file_contents/VERSION @@ -0,0 +1 @@ +1.2.0 diff --git a/test cases/common/241 get_file_contents/meson.build b/test cases/common/241 get_file_contents/meson.build new file mode 100644 index 0000000..a8c68d6 --- /dev/null +++ b/test cases/common/241 get_file_contents/meson.build @@ -0,0 +1,21 @@ +project( + 'meson-fs-read-file', + [], + version: files('VERSION') +) +fs = import('fs') + +assert(fs.read('VERSION').strip() == meson.project_version(), 'file misread') + +expected = ( + '∮ E⋅da = Q, n → ∞, ∑ f(i) = ∏ g(i), ∀x∈ℝ: ⌈x⌉ = −⌊−x⌋, α ∧ ¬β = ¬(¬α ∨ β)' +) +assert( + fs.read('utf-16-text', encoding: 'utf-16').strip() == expected, + 'file was not decoded correctly' +) + +# Make sure we handle `files()` objects properly, too +version_file = files('VERSION') + +subdir('other') diff --git a/test cases/common/241 get_file_contents/other/meson.build b/test cases/common/241 get_file_contents/other/meson.build new file mode 100644 index 0000000..9a7e4be --- /dev/null +++ b/test cases/common/241 get_file_contents/other/meson.build @@ -0,0 +1,3 @@ +fs = import('fs') +assert(fs.read(version_file).strip() == '1.2.0') +assert(fs.read('../VERSION').strip() == '1.2.0') diff --git a/test cases/common/241 get_file_contents/utf-16-text b/test cases/common/241 get_file_contents/utf-16-text Binary files differnew file mode 100644 index 0000000..ed1fefe --- /dev/null +++ b/test cases/common/241 get_file_contents/utf-16-text diff --git a/test cases/common/52 run target/check-env.py b/test cases/common/52 run target/check-env.py new file mode 100644 index 0000000..8df3e28 --- /dev/null +++ b/test cases/common/52 run target/check-env.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 + +import os + +assert 'MESON_SOURCE_ROOT' in os.environ +assert 'MESON_BUILD_ROOT' in os.environ +assert 'MESON_SUBDIR' in os.environ +assert 'MESONINTROSPECT' in os.environ +assert 'MY_ENV' in os.environ diff --git a/test cases/common/52 run target/meson.build b/test cases/common/52 run target/meson.build index 9abe698..a28d218 100644 --- a/test cases/common/52 run target/meson.build +++ b/test cases/common/52 run target/meson.build @@ -72,3 +72,9 @@ run_target('ctags', run_target('clang-format', command : converter) + +# Check we can pass env to the program +run_target('check-env', + command: [find_program('check-env.py')], + env: {'MY_ENV': '1'}, +) |