diff options
| author | Xavier Claessens <xclaessens@netflix.com> | 2025-10-11 15:23:49 -0400 |
|---|---|---|
| committer | Xavier Claessens <xclaesse@gmail.com> | 2025-10-15 12:15:39 -0400 |
| commit | 1826cba8d8f1316b83bb5864b9a61d756fe7f0ea (patch) | |
| tree | 163093278f92446292b8136c40ad90c8fbd0d5a8 | |
| parent | e0fc33dce2511c60c070064ffd86c746676dd302 (diff) | |
| download | meson-1826cba8d8f1316b83bb5864b9a61d756fe7f0ea.tar.gz | |
Add local_program() function
| -rw-r--r-- | data/syntax-highlighting/vim/syntax/meson.vim | 1 | ||||
| -rw-r--r-- | docs/markdown/snippets/local_program.md | 13 | ||||
| -rw-r--r-- | docs/yaml/functions/local_program.yaml | 36 | ||||
| -rw-r--r-- | mesonbuild/ast/interpreter.py | 1 | ||||
| -rw-r--r-- | mesonbuild/build.py | 44 | ||||
| -rw-r--r-- | mesonbuild/interpreter/interpreter.py | 20 | ||||
| -rw-r--r-- | mesonbuild/interpreter/interpreterobjects.py | 13 | ||||
| -rw-r--r-- | mesonbuild/interpreter/kwargs.py | 6 | ||||
| -rw-r--r-- | test cases/common/285 local program/meson.build | 40 | ||||
| -rwxr-xr-x | test cases/common/285 local program/prog.py | 5 | ||||
| -rw-r--r-- | test cases/common/285 local program/pymod.py.in | 2 |
11 files changed, 181 insertions, 0 deletions
diff --git a/data/syntax-highlighting/vim/syntax/meson.vim b/data/syntax-highlighting/vim/syntax/meson.vim index 905e9fe64..c4181c3f1 100644 --- a/data/syntax-highlighting/vim/syntax/meson.vim +++ b/data/syntax-highlighting/vim/syntax/meson.vim @@ -93,6 +93,7 @@ syn keyword mesonBuiltin \ executable \ files \ find_program + \ local_program \ generator \ get_option \ get_variable diff --git a/docs/markdown/snippets/local_program.md b/docs/markdown/snippets/local_program.md new file mode 100644 index 000000000..e671efb2c --- /dev/null +++ b/docs/markdown/snippets/local_program.md @@ -0,0 +1,13 @@ +## New [[local_program]] function + +Similar to [[find_program]], but only work with a program that exists in +source tree, or a built target. Meson will not look for the program in the +system or in a subproject. + +In addition, `depends` keyword argument can be specified in case the program +depends on built targets, for example a Python script could require a compiled +C module. If any such dependency is present, the program can only be used in +build-time commands (e.g. [[custom_target]]). + +The program can be passed to [[meson.override_find_program]] and used in +subprojects. diff --git a/docs/yaml/functions/local_program.yaml b/docs/yaml/functions/local_program.yaml new file mode 100644 index 000000000..d91867b02 --- /dev/null +++ b/docs/yaml/functions/local_program.yaml @@ -0,0 +1,36 @@ +name: local_program +returns: external_program +since: 1.10.0 +description: | + Similar to [[find_program]], but only work with a program that exists in + source tree, or a built target. Meson will not look for the program in the + system or in a subproject. + + In addition, `depends` keyword argument can be specified in case the program + depends on built targets, for example a Python script could require a compiled + C module. If any such dependency is present, the program can only be used in + build-time commands (e.g. [[custom_target]]). + + The program can be passed to [[meson.override_find_program]] and used in + subprojects. + +posargs: + program: + type: str | file | exe | custom_tgt | custom_idx + description: | + A [[@file]] object or the name of a program in the current source directory. + +kwargs: + depend_files: + type: array[str | file] + description: | + files ([[@str]], + [[@file]], or the return value of [[configure_file]] that + this target depends on. Useful for adding regen dependencies. + + depends: + type: array[build_tgt | custom_tgt | custom_idx] + description: | + Specifies that this target depends on the specified + target(s). If specified, this program can only be used at build time, + after those targets have been built. diff --git a/mesonbuild/ast/interpreter.py b/mesonbuild/ast/interpreter.py index 2f82a4a84..17ca15dd7 100644 --- a/mesonbuild/ast/interpreter.py +++ b/mesonbuild/ast/interpreter.py @@ -200,6 +200,7 @@ class AstInterpreter(InterpreterBase): 'configuration_data': self.func_do_nothing, 'configure_file': self.func_do_nothing, 'find_program': self.func_do_nothing, + 'local_program': self.func_do_nothing, 'include_directories': self.func_do_nothing, 'add_global_arguments': self.func_do_nothing, 'add_global_link_arguments': self.func_do_nothing, diff --git a/mesonbuild/build.py b/mesonbuild/build.py index 832e07711..a198b3973 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -3357,6 +3357,50 @@ class OverrideExecutable(Executable): def get_version(self, interpreter: T.Optional[Interpreter] = None) -> str: return self._version +class LocalProgram(HoldableObject): + ''' A wrapper for a program that may have build dependencies.''' + def __init__(self, program: T.Union[programs.ExternalProgram, Executable, CustomTarget, CustomTargetIndex], version: str, + depends: T.Optional[T.List[T.Union[BuildTarget, CustomTarget]]] = None, + depend_files: T.Optional[T.List[File]] = None) -> None: + super().__init__() + if isinstance(program, CustomTarget): + if len(program.outputs) != 1: + raise InvalidArguments('CustomTarget used as LocalProgram must have exactly one output.') + self.name = program.name + self.program = program + self.depends = list(depends or []) + self.depend_files = list(depend_files or []) + self.version = version + + def found(self) -> bool: + return True + + def get_version(self, interpreter: T.Optional[Interpreter] = None) -> str: + return self.version + + def get_command(self) -> T.List[str]: + if isinstance(self.program, (Executable, CustomTarget, CustomTargetIndex)): + return [os.path.join(self.program.subdir, self.program.get_filename())] + return self.program.get_command() + + def get_path(self) -> str: + if isinstance(self.program, (Executable, CustomTarget, CustomTargetIndex)): + return os.path.join(self.program.subdir, self.program.get_filename()) + return self.program.get_path() + + def description(self) -> str: + if isinstance(self.program, Executable): + return self.program.name + if isinstance(self.program, (CustomTarget, CustomTargetIndex)): + return self.program.get_filename() + return self.program.description() + + def run_program(self) -> T.Optional[programs.ExternalProgram]: + ''' Returns an ExternalProgram if it can be run at configure time.''' + if isinstance(self.program, programs.ExternalProgram) and not self.depends: + return self.program + return None + # A bit poorly named, but this represents plain data files to copy # during install. @dataclass(eq=False) diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index 631890d0f..20a6f8231 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -369,6 +369,7 @@ class Interpreter(InterpreterBase, HoldableObject): 'executable': self.func_executable, 'files': self.func_files, 'find_program': self.func_find_program, + 'local_program': self.func_local_program, 'generator': self.func_generator, 'get_option': self.func_get_option, 'get_variable': self.func_get_variable, @@ -441,6 +442,7 @@ class Interpreter(InterpreterBase, HoldableObject): build.GeneratedList: OBJ.GeneratedListHolder, build.ExtractedObjects: OBJ.GeneratedObjectsHolder, build.OverrideExecutable: OBJ.OverrideExecutableHolder, + build.LocalProgram: OBJ.LocalProgramHolder, build.RunTarget: OBJ.RunTargetHolder, build.AliasTarget: OBJ.AliasTargetHolder, build.Headers: OBJ.HeadersHolder, @@ -1784,6 +1786,24 @@ class Interpreter(InterpreterBase, HoldableObject): silent=False, wanted=kwargs['version'], version_arg=kwargs['version_argument'], search_dirs=search_dirs) + @FeatureNew('local_program', '1.10.0') + @typed_pos_args('local_program', (str, mesonlib.File, build.Executable, build.CustomTarget, build.CustomTargetIndex)) + @typed_kwargs( + 'local_program', + DEPENDS_KW, + DEPEND_FILES_KW, + ) + def func_local_program(self, node: mparser.BaseNode, args: T.Tuple[T.Union[mesonlib.FileOrString, build.Executable, build.CustomTarget, build.CustomTargetIndex]], + kwargs: kwtypes.LocalProgram) -> build.LocalProgram: + depends = [d.target if isinstance(d, build.CustomTargetIndex) else d for d in kwargs['depends']] + depend_files = self.source_strings_to_files(kwargs['depend_files']) + exe = args[0] + if isinstance(exe, (str, mesonlib.File)): + file = self.source_strings_to_files([exe])[0] + abspath = file.absolute_path(self.environment.source_dir, self.environment.build_dir) + exe = ExternalProgram(file.fname, command=[abspath], silent=True) + return build.LocalProgram(exe, self.project_version, depends, depend_files) + # When adding kwargs, please check if they make sense in dependencies.get_dep_identifier() @FeatureNewKwargs('dependency', '0.57.0', ['cmake_package_version']) @FeatureNewKwargs('dependency', '0.56.0', ['allow_fallback']) diff --git a/mesonbuild/interpreter/interpreterobjects.py b/mesonbuild/interpreter/interpreterobjects.py index 86e8957bc..82d0a75df 100644 --- a/mesonbuild/interpreter/interpreterobjects.py +++ b/mesonbuild/interpreter/interpreterobjects.py @@ -1171,3 +1171,16 @@ class OverrideExecutableHolder(BuildTargetHolder[build.OverrideExecutable]): @InterpreterObject.method('version') def version_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> str: return self.held_object.get_version(self.interpreter) + +class LocalProgramHolder(ObjectHolder[build.LocalProgram]): + @noPosargs + @noKwargs + @InterpreterObject.method('version') + def version_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> str: + return self.held_object.version + + @noPosargs + @noKwargs + @InterpreterObject.method('found') + def found_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> bool: + return True diff --git a/mesonbuild/interpreter/kwargs.py b/mesonbuild/interpreter/kwargs.py index 43e3cb30b..1d58aa61f 100644 --- a/mesonbuild/interpreter/kwargs.py +++ b/mesonbuild/interpreter/kwargs.py @@ -246,6 +246,12 @@ class FindProgram(ExtractRequired, ExtractSearchDirs): version: T.List[str] +class LocalProgram(TypedDict): + + depend_files: T.List[FileOrString] + depends: T.List[T.Union[build.BuildTarget, build.CustomTarget, build.CustomTargetIndex]] + + class RunCommand(TypedDict): check: bool diff --git a/test cases/common/285 local program/meson.build b/test cases/common/285 local program/meson.build new file mode 100644 index 000000000..e7a4a98e9 --- /dev/null +++ b/test cases/common/285 local program/meson.build @@ -0,0 +1,40 @@ +project('local program', version: '2.0') + +python3 = find_program('python3') + +# A module imported by prog but only available at build time. +pymod = custom_target( + input: 'pymod.py.in', + output: 'pymod.py', + command: [python3, '-c', 'import shutil,sys;shutil.copy(sys.argv[1], sys.argv[2])', '@INPUT@', '@OUTPUT@'], + build_by_default: false, +) + +# Copy into builddir to have the same location as pymod.py +prog = configure_file( + input: 'prog.py', + output: 'prog.py', + copy: true, +) + +# Without the dependency it can't be run, but it should have the project version. +prog1 = local_program(prog) +assert(prog1.version() == '2.0') +assert(prog1.found()) + +prog2 = local_program(prog, depends: pymod) +assert(prog2.version() == '2.0') +assert(prog2.found()) + +meson.override_find_program('prog', prog2) +prog3 = find_program('prog') +assert(prog3.version() == '2.0') +assert(prog3.found()) + +# This should have the pymod dependency +custom_target( + output: 'out.txt', + command: [prog3], + capture: true, + build_by_default: true, +) diff --git a/test cases/common/285 local program/prog.py b/test cases/common/285 local program/prog.py new file mode 100755 index 000000000..bd1042a2f --- /dev/null +++ b/test cases/common/285 local program/prog.py @@ -0,0 +1,5 @@ +#! /usr/bin/env python3 + +import pymod + +raise SystemExit(pymod.foo()) diff --git a/test cases/common/285 local program/pymod.py.in b/test cases/common/285 local program/pymod.py.in new file mode 100644 index 000000000..3cc1da0c3 --- /dev/null +++ b/test cases/common/285 local program/pymod.py.in @@ -0,0 +1,2 @@ +def foo() -> None: + return 0 |
