summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorXavier Claessens <xclaessens@netflix.com>2025-10-11 15:23:49 -0400
committerXavier Claessens <xclaesse@gmail.com>2025-10-15 12:15:39 -0400
commit1826cba8d8f1316b83bb5864b9a61d756fe7f0ea (patch)
tree163093278f92446292b8136c40ad90c8fbd0d5a8
parente0fc33dce2511c60c070064ffd86c746676dd302 (diff)
downloadmeson-1826cba8d8f1316b83bb5864b9a61d756fe7f0ea.tar.gz
Add local_program() function
-rw-r--r--data/syntax-highlighting/vim/syntax/meson.vim1
-rw-r--r--docs/markdown/snippets/local_program.md13
-rw-r--r--docs/yaml/functions/local_program.yaml36
-rw-r--r--mesonbuild/ast/interpreter.py1
-rw-r--r--mesonbuild/build.py44
-rw-r--r--mesonbuild/interpreter/interpreter.py20
-rw-r--r--mesonbuild/interpreter/interpreterobjects.py13
-rw-r--r--mesonbuild/interpreter/kwargs.py6
-rw-r--r--test cases/common/285 local program/meson.build40
-rwxr-xr-xtest cases/common/285 local program/prog.py5
-rw-r--r--test cases/common/285 local program/pymod.py.in2
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