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 <subdir/configure.h>

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 <keithp@keithp.com>
pull/14002/head
Keith Packard 3 months ago
parent df79a51571
commit bfc6e61ed4
  1. 17
      docs/yaml/functions/_build_target_base.yaml
  2. 17
      docs/yaml/functions/configure_file.yaml
  3. 13
      mesonbuild/backend/backends.py
  4. 24
      mesonbuild/build.py
  5. 31
      mesonbuild/interpreter/interpreter.py
  6. 1
      mesonbuild/interpreter/type_checking.py
  7. 10
      test cases/common/109 custom target capture/meson.build
  8. 3
      test cases/common/109 custom target capture/test.json
  9. 8
      test cases/common/117 shared module/meson.build
  10. 5
      test cases/common/117 shared module/test.json
  11. 7
      test cases/common/14 configure file/meson.build
  12. 3
      test cases/common/14 configure file/test.json

@ -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.

@ -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.

@ -335,9 +335,9 @@ class Backend:
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)
@ -372,9 +372,12 @@ class Backend:
# 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:
@ -484,7 +487,7 @@ class Backend:
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:
@ -1293,7 +1296,7 @@ class Backend:
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), ':')

@ -79,6 +79,7 @@ cs_kwargs = {'resources', 'cs_args'}
buildtarget_kwargs = {
'build_by_default',
'build_rpath',
'build_subdir',
'dependencies',
'extra_files',
'gui_app',
@ -522,6 +523,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:
@ -539,6 +541,9 @@ class Target(HoldableObject, metaclass=abc.ABCMeta):
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:
@ -599,6 +604,12 @@ class Target(HoldableObject, metaclass=abc.ABCMeta):
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.
@ -633,7 +644,7 @@ class Target(HoldableObject, metaclass=abc.ABCMeta):
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:
@ -704,7 +715,7 @@ class BuildTarget(Target):
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] = []
@ -2635,10 +2646,11 @@ class CustomTarget(Target, CustomTargetBase, CommandBase):
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(
@ -3011,6 +3023,12 @@ class CustomTargetIndex(CustomTargetBase, HoldableObject):
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

@ -2052,6 +2052,7 @@ class Interpreter(InterpreterBase, HoldableObject):
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:
@ -2142,7 +2143,8 @@ class Interpreter(InterpreterBase, HoldableObject):
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
@ -2640,6 +2642,7 @@ class Interpreter(InterpreterBase, HoldableObject):
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):
@ -2695,8 +2698,19 @@ class Interpreter(InterpreterBase, HoldableObject):
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:
@ -2712,7 +2726,6 @@ class Interpreter(InterpreterBase, HoldableObject):
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,
@ -3223,11 +3236,21 @@ class Interpreter(InterpreterBase, HoldableObject):
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:

@ -572,6 +572,7 @@ _ALL_TARGET_KWS: T.List[KwargInfo] = [
('1.1.0', 'generated sources as positional "objects" arguments')
},
),
KwargInfo('build_subdir', str, default='', since='1.7.0')
]

@ -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'
)

@ -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"}
]
}

@ -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'],

@ -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"}
]
}

@ -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())

@ -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"}
]
}

Loading…
Cancel
Save