From 153d476d84bfce63b86cb6fc5c5a868dfaf81947 Mon Sep 17 00:00:00 2001 From: Tristan Partin Date: Mon, 11 Dec 2023 19:36:28 -0600 Subject: [PATCH] Allow overriding programs from the command line People coming from autotools are used to having easy access for overriding programs. This should ease that transition. --- data/shell-completions/bash/meson | 2 ++ mesonbuild/ast/introspection.py | 2 ++ mesonbuild/coredata.py | 27 +++++++++++++++++++ mesonbuild/interpreter/interpreter.py | 26 ++++++++++++++++++ mesonbuild/msetup.py | 12 +++++++++ mesonbuild/programs.py | 2 +- test cases/unit/122 program overrides/cd.sh | 5 ++++ .../unit/122 program overrides/meson.build | 14 ++++++++++ .../122 program overrides/meson_options.txt | 1 + unittests/baseplatformtests.py | 6 +++++ unittests/platformagnostictests.py | 18 +++++++++++++ 11 files changed, 114 insertions(+), 1 deletion(-) create mode 100755 test cases/unit/122 program overrides/cd.sh create mode 100644 test cases/unit/122 program overrides/meson.build create mode 100644 test cases/unit/122 program overrides/meson_options.txt diff --git a/data/shell-completions/bash/meson b/data/shell-completions/bash/meson index 55c9c008d98c..86f7bf1f2ff0 100644 --- a/data/shell-completions/bash/meson +++ b/data/shell-completions/bash/meson @@ -270,6 +270,8 @@ _meson_common_setup_configure_longopts=( cmake-prefix-path build.cmake-prefix-path clearcache + native-program + cross-program ) _meson-setup() { diff --git a/mesonbuild/ast/introspection.py b/mesonbuild/ast/introspection.py index f9a6e11c0eb4..cf6b9fc6d70c 100644 --- a/mesonbuild/ast/introspection.py +++ b/mesonbuild/ast/introspection.py @@ -35,6 +35,8 @@ class IntrospectionHelper: def __init__(self, cross_file: T.Optional[str]): self.cross_file = [cross_file] if cross_file is not None else [] self.native_file: T.List[str] = [] + self.cross_program: T.List[str] = [] + self.native_program: T.List[str] = [] self.cmd_line_options: T.Dict[OptionKey, str] = {} self.projectoptions: T.List[str] = [] diff --git a/mesonbuild/coredata.py b/mesonbuild/coredata.py index 658ae47b1118..f4379b8e7bf2 100644 --- a/mesonbuild/coredata.py +++ b/mesonbuild/coredata.py @@ -55,6 +55,8 @@ class SharedCMDOptions(Protocol): projectoptions: T.List[str] cross_file: T.List[str] native_file: T.List[str] + cross_program: T.List[str] + native_program: T.List[str] OptionDictType = T.Union[T.Dict[str, 'UserOption[T.Any]'], 'OptionsView'] MutableKeyedOptionDictType = T.Dict['OptionKey', 'UserOption[T.Any]'] @@ -581,6 +583,9 @@ def __init__(self, options: SharedCMDOptions, scratch_dir: str, meson_command: T self.options: 'MutableKeyedOptionDictType' = {} self.cross_files = self.__load_config_files(options, scratch_dir, 'cross') self.compilers: PerMachine[T.Dict[str, Compiler]] = PerMachine(OrderedDict(), OrderedDict()) + self.program_overrides: PerMachine[T.Dict[str, str]] = PerMachine( + self.__parse_program_overrides(options, MachineChoice.BUILD), + self.__parse_program_overrides(options, MachineChoice.HOST)) # Set of subprojects that have already been initialized once, this is # required to be stored and reloaded with the coredata, as we don't @@ -604,6 +609,25 @@ def __init__(self, options: SharedCMDOptions, scratch_dir: str, meson_command: T self.builtin_options_libdir_cross_fixup() self.init_builtins('') + @staticmethod + def __parse_program_overrides(options: SharedCMDOptions, for_machine: MachineChoice) -> OrderedDict[str, str]: + overrides_map: OrderedDict[str, str] = OrderedDict() + overrides: T.List[str] + if for_machine == MachineChoice.BUILD: + overrides = options.native_program + elif for_machine == MachineChoice.HOST: + overrides = options.cross_program + + for o in overrides: + parts = o.split('=', 1) + if len(parts) == 1: + word = 'native' if for_machine == MachineChoice.HOST else 'cross' + raise MesonException(f'Invalid value for --{word}-program: {o}. The override name and value must be separated by an "=" in the form of xxx=path/to/yyy.') + + overrides_map[parts[0]] = parts[1] + + return overrides_map + @staticmethod def __load_config_files(options: SharedCMDOptions, scratch_dir: str, ftype: str) -> T.List[str]: # Need to try and make the passed filenames absolute because when the @@ -1051,6 +1075,9 @@ def emit_base_options_warnings(self, enabled_opts: T.List[OptionKey]) -> None: mlog.warning('Base option \'b_bitcode\' is enabled, which is incompatible with many linker options. Incompatible options such as \'b_asneeded\' have been disabled.', fatal=False) mlog.warning('Please see https://mesonbuild.com/Builtin-options.html#Notes_about_Apple_Bitcode_support for more details.', fatal=False) + def get_program_overrides(self, for_machine: MachineChoice) -> T.Dict[str, str]: + return self.program_overrides[for_machine] + class CmdLineFileParser(configparser.ConfigParser): def __init__(self) -> None: # We don't want ':' as key delimiter, otherwise it would break when diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index 5603b99375c0..cab215167930 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -1539,6 +1539,28 @@ def add_languages_for(self, args: T.List[str], required: bool, for_machine: Mach return success + def program_from_override(self, for_machine: MachineChoice, prognames: T.List[mesonlib.FileOrString], + extra_info: T.List[mlog.TV_Loggable]) -> T.Optional[ExternalProgram]: + program_overrides = self.coredata.get_program_overrides(for_machine) + if not program_overrides: + return None + + for p in prognames: + if isinstance(p, mesonlib.File): + continue # Always points to a local (i.e. self generated) file. + if not isinstance(p, str): + raise InterpreterException('Executable name must be a string') + + override = program_overrides.get(p, None) + if not override: + return None + + prog = ExternalProgram(p, command=override, silent=True) + if not isinstance(prog, NonExistingExternalProgram): + extra_info.append(mlog.blue('(overridden)')) + return prog + return None + def program_from_file_for(self, for_machine: MachineChoice, prognames: T.List[mesonlib.FileOrString] ) -> T.Optional[ExternalProgram]: for p in prognames: @@ -1656,6 +1678,10 @@ def program_lookup(self, args: T.List[mesonlib.FileOrString], for_machine: Machi if progobj: return progobj + progobj = self.program_from_override(for_machine, args, extra_info) + if progobj: + return progobj + if args[0] == 'meson': # Override find_program('meson') to return what we were invoked with return ExternalProgram('meson', self.environment.get_build_command(), silent=True) diff --git a/mesonbuild/msetup.py b/mesonbuild/msetup.py index c1d71e2e55e2..c21f6e7bbf90 100644 --- a/mesonbuild/msetup.py +++ b/mesonbuild/msetup.py @@ -9,6 +9,8 @@ from pathlib import Path import typing as T +from mesonbuild.utils.universal import MachineChoice + from . import build, coredata, environment, interpreter, mesonlib, mintro, mlog from .mesonlib import MesonException @@ -65,6 +67,14 @@ def add_arguments(parser: argparse.ArgumentParser) -> None: 'newer version of meson.') parser.add_argument('--clearcache', action='store_true', default=False, help='Clear cached state (e.g. found dependencies). Since 1.3.0.') + parser.add_argument('--native-program', + default=[], + action='append', + help='Provide a native program override. The override must be of the form binary=/path/to/override. Since 1.4.0.') + parser.add_argument('--cross-program', + default=[], + action='append', + help='Provide a cross program override. The override must be of the form binary=/path/to/override. Since 1.4.0.') parser.add_argument('builddir', nargs='?', default=None) parser.add_argument('sourcedir', nargs='?', default=None) @@ -256,6 +266,8 @@ def _generate(self, env: environment.Environment, capture: bool, vslite_ctx: T.O # read from a pipe and wrote into a private file. self.options.cross_file = env.coredata.cross_files self.options.native_file = env.coredata.config_files + self.options.cross_program = list(map(lambda i: f'{i[0]}={i[1]}', env.coredata.get_program_overrides(MachineChoice.HOST).items())) + self.options.native_program = list(map(lambda i: f'{i[0]}={i[1]}', env.coredata.get_program_overrides(MachineChoice.BUILD).items())) coredata.write_cmd_line_file(self.build_dir, self.options) else: coredata.update_cmd_line_file(self.build_dir, self.options) diff --git a/mesonbuild/programs.py b/mesonbuild/programs.py index b73f9e4025df..f2235143038e 100644 --- a/mesonbuild/programs.py +++ b/mesonbuild/programs.py @@ -30,7 +30,7 @@ class ExternalProgram(mesonlib.HoldableObject): windows_exts = ('exe', 'msc', 'com', 'bat', 'cmd') for_machine = MachineChoice.BUILD - def __init__(self, name: str, command: T.Optional[T.List[str]] = None, + def __init__(self, name: str, command: T.Optional[T.Union[str, T.List[str]]] = None, silent: bool = False, search_dir: T.Optional[str] = None, extra_search_dirs: T.Optional[T.List[str]] = None): self.name = name diff --git a/test cases/unit/122 program overrides/cd.sh b/test cases/unit/122 program overrides/cd.sh new file mode 100755 index 000000000000..1f3879caef13 --- /dev/null +++ b/test cases/unit/122 program overrides/cd.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +echo "This is the wrapped cd" + +cd "$1" || exit 1 diff --git a/test cases/unit/122 program overrides/meson.build b/test cases/unit/122 program overrides/meson.build new file mode 100644 index 000000000000..c391c9beffce --- /dev/null +++ b/test cases/unit/122 program overrides/meson.build @@ -0,0 +1,14 @@ +project('program overrides') + +fs = import('fs') + +native_cd = find_program('cd', native: true) +cross_cd = find_program('cd', native: false) + +if get_option('machine') == 'build' + assert(fs.name(native_cd.full_path()) == 'cd.sh', 'override did not occur') + assert(fs.name(cross_cd.full_path()) == 'cd', 'override did not occur') +else + assert(fs.name(cross_cd.full_path()) == 'cd.sh', 'override did not occur') + assert(fs.name(native_cd.full_path()) == 'cd', 'override did not occur') +endif diff --git a/test cases/unit/122 program overrides/meson_options.txt b/test cases/unit/122 program overrides/meson_options.txt new file mode 100644 index 000000000000..6f4ffb12aaae --- /dev/null +++ b/test cases/unit/122 program overrides/meson_options.txt @@ -0,0 +1 @@ +option('machine', type: 'combo', choices: ['host', 'build'], value: 'host') diff --git a/unittests/baseplatformtests.py b/unittests/baseplatformtests.py index ec3f18908466..0ee5c9d6a7ce 100644 --- a/unittests/baseplatformtests.py +++ b/unittests/baseplatformtests.py @@ -55,6 +55,8 @@ def setUp(self): self.meson_args = ['--backend=' + self.backend_name] self.meson_native_files = [] self.meson_cross_files = [] + self.meson_native_programs = [] + self.meson_cross_programs = [] self.meson_command = python_command + [get_meson_script()] self.setup_command = self.meson_command + ['setup'] + self.meson_args self.mconf_command = self.meson_command + ['configure'] @@ -207,6 +209,10 @@ def init(self, srcdir, *, args += ['--native-file', f] for f in self.meson_cross_files: args += ['--cross-file', f] + for o in self.meson_native_programs: + args += ['--native-program', o] + for o in self.meson_cross_programs: + args += ['--cross-program', o] self.privatedir = os.path.join(self.builddir, 'meson-private') if inprocess: try: diff --git a/unittests/platformagnostictests.py b/unittests/platformagnostictests.py index 581ecacccabe..e35b280c3344 100644 --- a/unittests/platformagnostictests.py +++ b/unittests/platformagnostictests.py @@ -275,3 +275,21 @@ def test_error_configuring_subdir(self): self.assertIn('first statement must be a call to project()', out) # provide guidance diagnostics by finding a file whose first AST statement is project() self.assertIn(f'Did you mean to run meson from the directory: "{testdir}"?', out) + + def test_native_program(self): + testdir = os.path.join(self.unit_test_dir, '122 program overrides') + + with self.assertRaises(subprocess.CalledProcessError): + self.init(testdir, extra_args=['--native-program', 'xxx']) + + my_cd = os.path.join(testdir, "cd.sh") + self.init(testdir, extra_args=['-Dmachine=build', '--native-program', f'cd={my_cd}']) + + def test_cross_program(self): + testdir = os.path.join(self.unit_test_dir, '122 program overrides') + + with self.assertRaises(subprocess.CalledProcessError): + self.init(testdir, extra_args=['--cross-program', 'xxx']) + + my_cd = os.path.join(testdir, "cd.sh") + self.init(testdir, extra_args=['-Dmachine=host', '--cross-program', f'cd={my_cd}'])