From 590b0ae2e1c2d3445ce4d103e24734fbca0e8937 Mon Sep 17 00:00:00 2001 From: Keith Packard Date: Tue, 10 Dec 2024 14:34:08 -0800 Subject: [PATCH] Add build target keyword parameter 'build_subdir' [v3] Place the build products in a directory of the specified name somewhere within the build directory. This allows use of the target that includes a specific directory name: #include This also allows creating targets with the same basename by using different subdirectory names. v2: Move build_subdir to Target class. Error if path separator in build_dir v3: Rename to 'build_subdir' to make it clear that the name is appended to a meson-specific build directory, and does not provide the user with a way to define the overall meson build heirarchy. Allow build_subdir to include path separators. Support 'build_subdir' for configure_file. build_subdir must not exist in the source directory and must not contain '..' Add documentation and tests Signed-off-by: Keith Packard --- docs/yaml/functions/_build_target_base.yaml | 17 ++++++++++ docs/yaml/functions/configure_file.yaml | 17 ++++++++++ mesonbuild/backend/backends.py | 13 +++++--- mesonbuild/build.py | 24 ++++++++++++-- mesonbuild/interpreter/interpreter.py | 31 ++++++++++++++++--- mesonbuild/interpreter/type_checking.py | 1 + .../109 custom target capture/meson.build | 10 ++++++ .../109 custom target capture/test.json | 3 +- .../common/117 shared module/meson.build | 8 +++++ test cases/common/117 shared module/test.json | 5 ++- .../common/14 configure file/meson.build | 7 +++++ test cases/common/14 configure file/test.json | 3 +- 12 files changed, 124 insertions(+), 15 deletions(-) diff --git a/docs/yaml/functions/_build_target_base.yaml b/docs/yaml/functions/_build_target_base.yaml index 1721b29cfe5a..e99300d067bd 100644 --- a/docs/yaml/functions/_build_target_base.yaml +++ b/docs/yaml/functions/_build_target_base.yaml @@ -328,3 +328,20 @@ kwargs: This allows renaming similar to the dependency renaming feature of cargo or `extern crate foo as bar` inside rust code. + + build_subdir: + type: str + since: 1.7.0 + description: + Places the build results in a subdirectory of the given name + rather than directly into the build directory. This does not + affect the install directory, which uses install_dir. + + This allows inserting a directory name into the build path, + either when needed to use the build result while building other + targets or as a way to support multiple targets with the same + basename by using unique build_subdir values for each one. + + build_subdir may not match a file or directory in the source + directory, nor may it include '..' to refer to the parent of the + build directory. diff --git a/docs/yaml/functions/configure_file.yaml b/docs/yaml/functions/configure_file.yaml index 20b96aa6e62d..ea963c7df27c 100644 --- a/docs/yaml/functions/configure_file.yaml +++ b/docs/yaml/functions/configure_file.yaml @@ -153,3 +153,20 @@ kwargs: description: | When specified, macro guards will be used instead of '#pragma once'. The macro guard name will be the specified name. + + build_subdir: + type: str + since: 1.7.0 + description: + Places the build results in a subdirectory of the given name + rather than directly into the build directory. This does not + affect the install directory, which uses install_dir. + + This allows inserting a directory name into the build path, + either when needed to use the build result while building other + targets or as a way to support multiple targets with the same + basename by using unique build_subdir values for each one. + + build_subdir may not match a file or directory in the source + directory, nor may it include '..' to refer to the parent of the + build directory. diff --git a/mesonbuild/backend/backends.py b/mesonbuild/backend/backends.py index a4be50f664b1..fc705a04dcd7 100644 --- a/mesonbuild/backend/backends.py +++ b/mesonbuild/backend/backends.py @@ -333,9 +333,9 @@ def get_source_dir_include_args(self, target: build.BuildTarget, compiler: 'Comp def get_build_dir_include_args(self, target: build.BuildTarget, compiler: 'Compiler', *, absolute_path: bool = False) -> T.List[str]: if absolute_path: - curdir = os.path.join(self.build_dir, target.get_subdir()) + curdir = os.path.join(self.build_dir, target.get_builddir()) else: - curdir = target.get_subdir() + curdir = target.get_builddir() if curdir == '': curdir = '.' return compiler.get_include_args(curdir, False) @@ -370,9 +370,12 @@ def get_target_dir(self, target: T.Union[build.Target, build.CustomTargetIndex]) # this produces no output, only a dummy top-level name dirname = '' elif self.environment.coredata.get_option(OptionKey('layout')) == 'mirror': - dirname = target.get_subdir() + dirname = target.get_builddir() else: dirname = 'meson-out' + build_subdir = target.get_build_subdir() + if build_subdir: + dirname = os.path.join(dirname, build_subdir) return dirname def get_target_dir_relative_to(self, t: build.Target, o: build.Target) -> str: @@ -482,7 +485,7 @@ def _flatten_object_list(self, target: build.BuildTarget, for obj in objects: if isinstance(obj, str): o = os.path.join(proj_dir_to_build_root, - self.build_to_src, target.get_subdir(), obj) + self.build_to_src, target.get_builddir(), obj) obj_list.append(o) elif isinstance(obj, mesonlib.File): if obj.is_built: @@ -1276,7 +1279,7 @@ def create_test_serialisation(self, tests: T.List['Test']) -> T.List[TestSeriali ld_lib_path_libs.add(l) env_build_dir = self.environment.get_build_dir() - ld_lib_path: T.Set[str] = set(os.path.join(env_build_dir, l.get_subdir()) for l in ld_lib_path_libs) + ld_lib_path: T.Set[str] = set(os.path.join(env_build_dir, l.get_builddir()) for l in ld_lib_path_libs) if ld_lib_path: t_env.prepend('LD_LIBRARY_PATH', list(ld_lib_path), ':') diff --git a/mesonbuild/build.py b/mesonbuild/build.py index 35f1f24a42f8..14a663ce66fb 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -78,6 +78,7 @@ class DFeatures(TypedDict): buildtarget_kwargs = { 'build_by_default', 'build_rpath', + 'build_subdir', 'dependencies', 'extra_files', 'gui_app', @@ -525,6 +526,7 @@ class Target(HoldableObject, metaclass=abc.ABCMeta): build_always_stale: bool = False extra_files: T.List[File] = field(default_factory=list) override_options: InitVar[T.Optional[T.Dict[OptionKey, str]]] = None + build_subdir: str = '' @abc.abstractproperty def typename(self) -> str: @@ -548,6 +550,9 @@ def __post_init__(self, overrides: T.Optional[T.Dict[OptionKey, str]]) -> None: Target "{self.name}" has a path separator in its name. This is not supported, it can cause unexpected failures and will become a hard error in the future.''')) + self.builddir = self.subdir + if self.build_subdir: + self.builddir = os.path.join(self.subdir, self.build_subdir) # dataclass comparators? def __lt__(self, other: object) -> bool: @@ -608,6 +613,12 @@ def get_subdir(self) -> str: def get_typename(self) -> str: return self.typename + def get_build_subdir(self) -> str: + return self.build_subdir + + def get_builddir(self) -> str: + return self.builddir + @staticmethod def _get_id_hash(target_id: str) -> str: # We don't really need cryptographic security here. @@ -642,7 +653,7 @@ def get_id(self) -> str: if getattr(self, 'name_suffix_set', False): name += '.' + self.suffix return self.construct_id_from_path( - self.subdir, name, self.type_suffix()) + self.builddir, name, self.type_suffix()) def process_kwargs_base(self, kwargs: T.Dict[str, T.Any]) -> None: if 'build_by_default' in kwargs: @@ -738,7 +749,7 @@ def __init__( environment: environment.Environment, compilers: T.Dict[str, 'Compiler'], kwargs: T.Dict[str, T.Any]): - super().__init__(name, subdir, subproject, True, for_machine, environment, install=kwargs.get('install', False)) + super().__init__(name, subdir, subproject, True, for_machine, environment, install=kwargs.get('install', False), build_subdir=kwargs.get('build_subdir', '')) self.all_compilers = compilers self.compilers: OrderedDict[str, Compiler] = OrderedDict() self.objects: T.List[ObjectTypes] = [] @@ -2647,10 +2658,11 @@ def __init__(self, absolute_paths: bool = False, backend: T.Optional['Backend'] = None, description: str = 'Generating {} with a custom command', + build_subdir: str = '', ): # TODO expose keyword arg to make MachineChoice.HOST configurable super().__init__(name, subdir, subproject, False, MachineChoice.HOST, environment, - install, build_always_stale) + install, build_always_stale, build_subdir = build_subdir) self.sources = list(sources) self.outputs = substitute_values( outputs, get_filenames_templates_dict( @@ -3027,6 +3039,12 @@ def get_outputs(self) -> T.List[str]: def get_subdir(self) -> str: return self.target.get_subdir() + def get_build_subdir(self) -> str: + return self.target.get_build_subdir() + + def get_builddir(self) -> str: + return self.target.get_builddir() + def get_filename(self) -> str: return self.output diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index d717485e8a1e..41886c2cab8c 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -2055,6 +2055,7 @@ def _validate_custom_target_outputs(self, has_multi_in: bool, outputs: T.Iterabl KwargInfo('feed', bool, default=False, since='0.59.0'), KwargInfo('capture', bool, default=False), KwargInfo('console', bool, default=False, since='0.48.0'), + KwargInfo('build_subdir', str, default='', since='1.7.0'), ) def func_custom_target(self, node: mparser.FunctionNode, args: T.Tuple[str], kwargs: 'kwtypes.CustomTarget') -> build.CustomTarget: @@ -2145,7 +2146,8 @@ def func_custom_target(self, node: mparser.FunctionNode, args: T.Tuple[str], install_dir=kwargs['install_dir'], install_mode=install_mode, install_tag=kwargs['install_tag'], - backend=self.backend) + backend=self.backend, + build_subdir=kwargs['build_subdir']) self.add_target(tg.name, tg) return tg @@ -2643,6 +2645,7 @@ def func_install_subdir(self, node: mparser.BaseNode, args: T.Tuple[str], KwargInfo('output_format', str, default='c', since='0.47.0', since_values={'json': '1.3.0'}, validator=in_set_validator({'c', 'json', 'nasm'})), KwargInfo('macro_name', (str, NoneType), default=None, since='1.3.0'), + KwargInfo('build_subdir', str, default='', since='1.7.0'), ) def func_configure_file(self, node: mparser.BaseNode, args: T.List[TYPE_var], kwargs: kwtypes.ConfigureFile): @@ -2698,8 +2701,19 @@ def func_configure_file(self, node: mparser.BaseNode, args: T.List[TYPE_var], mlog.warning('Output file', mlog.bold(ofile_rpath, True), 'for configure_file() at', current_call, 'overwrites configure_file() output at', first_call) else: self.configure_file_outputs[ofile_rpath] = self.current_node.lineno - (ofile_path, ofile_fname) = os.path.split(os.path.join(self.subdir, output)) + + # Validate build_subdir + build_subdir = kwargs['build_subdir'] + self.build_subdir = build_subdir + if self.build_subdir and self.build_subdir != '.': + if os.path.exists(os.path.join(self.source_root, self.subdir, build_subdir)): + raise InvalidArguments(f'Build subdir "{build_subdir}" in output "{output}" exists in source tree.') + if '..' in build_subdir: + raise InvalidArguments(f'Build subdir "{build_subdir}" in output "{output}" contains ..') + + (ofile_path, ofile_fname) = os.path.split(os.path.join(self.subdir, self.build_subdir, output)) ofile_abs = os.path.join(self.environment.build_dir, ofile_path, ofile_fname) + os.makedirs(os.path.split(ofile_abs)[0], exist_ok=True) # Perform the appropriate action if kwargs['configuration'] is not None: @@ -2715,7 +2729,6 @@ def func_configure_file(self, node: mparser.BaseNode, args: T.List[TYPE_var], if len(inputs) > 1: raise InterpreterException('At most one input file can given in configuration mode') if inputs: - os.makedirs(os.path.join(self.environment.build_dir, self.subdir), exist_ok=True) file_encoding = kwargs['encoding'] missing_variables, confdata_useless = \ mesonlib.do_conf_file(inputs_abs[0], ofile_abs, conf, @@ -3224,11 +3237,21 @@ def add_target(self, name: str, tobj: build.Target) -> None: To define a target that builds in that directory you must define it in the meson.build file in that directory. ''')) + + # Make sure build_subdir doesn't exist in the source tree and + # doesn't contain .. + build_subdir = tobj.get_build_subdir() + if build_subdir and build_subdir != '.': + if os.path.exists(os.path.join(self.source_root, self.subdir, build_subdir)): + raise InvalidArguments(f'Build subdir "{build_subdir}" in target "{name}" exists in source tree.') + if '..' in build_subdir: + raise InvalidArguments(f'Build subdir "{build_subdir}" in target "{name}" contains ..') + self.validate_forbidden_targets(name) # To permit an executable and a shared library to have the # same name, such as "foo.exe" and "libfoo.a". idname = tobj.get_id() - subdir = tobj.get_subdir() + subdir = tobj.get_builddir() namedir = (name, subdir) if idname in self.build.targets: diff --git a/mesonbuild/interpreter/type_checking.py b/mesonbuild/interpreter/type_checking.py index ed34be950065..0911ce3831bc 100644 --- a/mesonbuild/interpreter/type_checking.py +++ b/mesonbuild/interpreter/type_checking.py @@ -584,6 +584,7 @@ def _objects_validator(vals: T.List[ObjectTypes]) -> T.Optional[str]: ('1.1.0', 'generated sources as positional "objects" arguments') }, ), + KwargInfo('build_subdir', str, default='', since='1.7.0') ] diff --git a/test cases/common/109 custom target capture/meson.build b/test cases/common/109 custom target capture/meson.build index b7622014a99d..9fd7e22aec1a 100644 --- a/test cases/common/109 custom target capture/meson.build +++ b/test cases/common/109 custom target capture/meson.build @@ -22,3 +22,13 @@ if not os.path.exists(sys.argv[1]): ''' test('capture-wrote', python3, args : ['-c', ct_output_exists, mytarget]) + +mytarget = custom_target('bindat', + output : 'data.dat', + input : 'data_source.txt', + build_subdir : 'subdir2', + capture : true, + command : [python3, comp, '@INPUT@'], + install : true, + install_dir : 'subdir2' +) diff --git a/test cases/common/109 custom target capture/test.json b/test cases/common/109 custom target capture/test.json index ba66b024aaa9..663a8f3201fc 100644 --- a/test cases/common/109 custom target capture/test.json +++ b/test cases/common/109 custom target capture/test.json @@ -1,5 +1,6 @@ { "installed": [ - {"type": "file", "file": "usr/subdir/data.dat"} + {"type": "file", "file": "usr/subdir/data.dat"}, + {"type": "file", "file": "usr/subdir2/data.dat"} ] } diff --git a/test cases/common/117 shared module/meson.build b/test cases/common/117 shared module/meson.build index 94d17a716da9..494ce42ee04c 100644 --- a/test cases/common/117 shared module/meson.build +++ b/test cases/common/117 shared module/meson.build @@ -34,6 +34,14 @@ test('import test', e, args : m) m2 = build_target('mymodule2', 'module.c', target_type: 'shared_module') test('import test 2', e, args : m2) +# Same as above, but built and installed in a sub directory +m2_subdir = build_target('mymodule2', 'module.c', + target_type: 'shared_module', + build_subdir: 'subdir', + install: true, + install_dir: join_paths(get_option('libdir'), 'modules/subdir')) +test('import test 2 subdir', e, args : m2_subdir) + # Shared module that does not export any symbols shared_module('nosyms', 'nosyms.c', override_options: ['werror=false'], diff --git a/test cases/common/117 shared module/test.json b/test cases/common/117 shared module/test.json index 33bfeff07600..b24149cf3df7 100644 --- a/test cases/common/117 shared module/test.json +++ b/test cases/common/117 shared module/test.json @@ -2,6 +2,9 @@ "installed": [ {"type": "expr", "file": "usr/lib/modules/libnosyms?so"}, {"type": "implibempty", "file": "usr/lib/modules/libnosyms"}, - {"type": "pdb", "file": "usr/lib/modules/nosyms"} + {"type": "pdb", "file": "usr/lib/modules/nosyms"}, + {"type": "expr", "file": "usr/lib/modules/subdir/libmymodule2?so"}, + {"type": "implib", "file": "usr/lib/modules/subdir/libmymodule2"}, + {"type": "pdb", "file": "usr/lib/modules/subdir/mymodule2"} ] } diff --git a/test cases/common/14 configure file/meson.build b/test cases/common/14 configure file/meson.build index 036a562b796c..399c5d561aea 100644 --- a/test cases/common/14 configure file/meson.build +++ b/test cases/common/14 configure file/meson.build @@ -30,6 +30,13 @@ configure_file(input : files('config.h.in'), output : 'config2.h', configuration : conf) +# Test if build_subdir works +configure_file(input : files('config.h.in'), + output : 'config2.h', + build_subdir : 'config-subdir', + install_dir : 'share/appdir/config-subdir', + configuration : conf) + # Now generate a header file with an external script. genprog = import('python3').find_python() scriptfile = '@0@/generator.py'.format(meson.current_source_dir()) diff --git a/test cases/common/14 configure file/test.json b/test cases/common/14 configure file/test.json index 5a6ccd57a8fb..51e67702889d 100644 --- a/test cases/common/14 configure file/test.json +++ b/test cases/common/14 configure file/test.json @@ -4,6 +4,7 @@ {"type": "file", "file": "usr/share/appdir/config2b.h"}, {"type": "file", "file": "usr/share/appdireh/config2-1.h"}, {"type": "file", "file": "usr/share/appdirok/config2-2.h"}, - {"type": "file", "file": "usr/share/configure file test/invalid-utf8-1.bin"} + {"type": "file", "file": "usr/share/configure file test/invalid-utf8-1.bin"}, + {"type": "file", "file": "usr/share/appdir/config-subdir/config2.h"} ] }