Skip to content

Commit

Permalink
build: Support 'rename' in build_target and custom_target
Browse files Browse the repository at this point in the history
This allows built results to be renamed during installation, just as
with install_data.

This feature is motivated by the need to install multiple files from a
single source directory into different install directories but with
the same basename. In my case, I build dozens of 'libc.a' files which
need to be installed into the toolchain's multilib heirarchy
correctly.

The rename parameter must either be empty, indicating that renaming is
disabled, or have length equal to the number of outputs
generated. Using an empty list to disable renaming matches the
behavior of install_data.

Signed-off-by: Keith Packard <[email protected]>
  • Loading branch information
keith-packard committed Dec 7, 2024
1 parent 9f3f88f commit c91e322
Show file tree
Hide file tree
Showing 13 changed files with 117 additions and 24 deletions.
1 change: 1 addition & 0 deletions cross/linux-mingw-w64-64bit.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ exe_wrapper = 'wine'
cmake = '/usr/bin/cmake'

[properties]
skip_sanity_check = true
# Directory that contains 'bin', 'lib', etc
root = '/usr/x86_64-w64-mingw32'
# Directory that contains 'bin', 'lib', etc for the toolchain and system libraries
Expand Down
10 changes: 10 additions & 0 deletions docs/yaml/functions/_build_target_base.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -328,3 +328,13 @@ kwargs:
This allows renaming similar to the dependency renaming feature of cargo
or `extern crate foo as bar` inside rust code.
rename:
type: list[str]
since: 1.7.0
description: |
If specified renames each output into corresponding file from
`rename` list during install. Nested paths are allowed and they
are joined with `install_dir`. The `rename` list must either be
empty (which disables this feature) or its length must be equal
to the number of outputs,
10 changes: 10 additions & 0 deletions docs/yaml/functions/custom_target.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -235,3 +235,13 @@ kwargs:
their input from a file and instead read it from standard input. When this
argument is set to `true`, Meson feeds the input file to `stdin`. Note that
your argument list may not contain `@INPUT@` when feed mode is active.
rename:
type: list[str]
since: 1.7.0
description: |
If specified renames each output into corresponding file from
`rename` list during install. Nested paths are allowed and they
are joined with `install_dir`. The `rename` list must either be
empty (which disables this feature) or its length must be equal
to the number of outputs,
20 changes: 12 additions & 8 deletions mesonbuild/backend/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,14 @@ class TargetInstallData:
optional: bool = False
tag: T.Optional[str] = None
can_strip: bool = False
out_fname: T.Optional[str] = None

def __post_init__(self, outdir_name: T.Optional[str]) -> None:
if outdir_name is None:
outdir_name = os.path.join('{prefix}', self.outdir)
self.out_name = os.path.join(outdir_name, os.path.basename(self.fname))
if self.out_fname is None:
self.out_fname = os.path.basename(self.fname)
self.out_name = os.path.join(outdir_name, self.out_fname)

@dataclass(eq=False)
class InstallEmptyDir:
Expand Down Expand Up @@ -1709,6 +1712,7 @@ def generate_target_install(self, d: InstallData) -> None:
if not t.should_install():
continue
outdirs, install_dir_names, custom_install_dir = t.get_install_dir()
rename = t.get_rename()
# Sanity-check the outputs and install_dirs
num_outdirs, num_out = len(outdirs), len(t.get_outputs())
if num_outdirs not in {1, num_out}:
Expand Down Expand Up @@ -1749,7 +1753,7 @@ def generate_target_install(self, d: InstallData) -> None:
first_outdir_name,
should_strip, mappings, t.rpath_dirs_to_remove,
t.install_rpath, install_mode, t.subproject,
tag=tag, can_strip=can_strip)
tag=tag, can_strip=can_strip, out_fname=rename[0])
d.targets.append(i)

for alias, to, tag in t.get_aliases():
Expand Down Expand Up @@ -1787,14 +1791,14 @@ def generate_target_install(self, d: InstallData) -> None:
d.targets.append(i)
# Install secondary outputs. Only used for Vala right now.
if num_outdirs > 1:
for output, outdir, outdir_name, tag in zip(t.get_outputs()[1:], outdirs[1:], install_dir_names[1:], t.install_tag[1:]):
for output, outdir, outdir_name, tag, out_fname in zip(t.get_outputs()[1:], outdirs[1:], install_dir_names[1:], t.install_tag[1:], rename[1:]):
# User requested that we not install this output
if outdir is False:
continue
f = os.path.join(self.get_target_dir(t), output)
i = TargetInstallData(f, outdir, outdir_name, False, {}, set(), None,
install_mode, t.subproject,
tag=tag)
tag=tag, out_fname=out_fname)
d.targets.append(i)
elif isinstance(t, build.CustomTarget):
# If only one install_dir is specified, assume that all
Expand All @@ -1809,16 +1813,16 @@ def generate_target_install(self, d: InstallData) -> None:
# to the length of outputs…
if num_outdirs == 1 and num_out > 1:
if first_outdir is not False:
for output, tag in zip(t.get_outputs(), t.install_tag):
for output, tag, out_fname in zip(t.get_outputs(), t.install_tag, rename):
tag = tag or self.guess_install_tag(output, first_outdir)
f = os.path.join(self.get_target_dir(t), output)
i = TargetInstallData(f, first_outdir, first_outdir_name,
False, {}, set(), None, install_mode,
t.subproject, optional=not t.build_by_default,
tag=tag)
tag=tag, out_fname=out_fname)
d.targets.append(i)
else:
for output, outdir, outdir_name, tag in zip(t.get_outputs(), outdirs, install_dir_names, t.install_tag):
for output, outdir, outdir_name, tag, out_fname in zip(t.get_outputs(), outdirs, install_dir_names, t.install_tag, rename):
# User requested that we not install this output
if outdir is False:
continue
Expand All @@ -1827,7 +1831,7 @@ def generate_target_install(self, d: InstallData) -> None:
i = TargetInstallData(f, outdir, outdir_name,
False, {}, set(), None, install_mode,
t.subproject, optional=not t.build_by_default,
tag=tag)
tag=tag, out_fname=out_fname)
d.targets.append(i)

def generate_custom_install_script(self, d: InstallData) -> None:
Expand Down
18 changes: 16 additions & 2 deletions mesonbuild/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ class DFeatures(TypedDict):
'native',
'objects',
'override_options',
'rename',
'sources',
'gnu_symbol_visibility',
'link_language',
Expand Down Expand Up @@ -1127,6 +1128,11 @@ def get_custom_install_dir(self) -> T.List[T.Union[str, Literal[False]]]:
def get_custom_install_mode(self) -> T.Optional['FileMode']:
return self.install_mode

def get_rename(self) -> T.List[str]:
if self.rename:
return self.rename
return [None] * len(self.outputs)

def process_kwargs(self, kwargs):
self.process_kwargs_base(kwargs)
self.original_kwargs = kwargs
Expand Down Expand Up @@ -1164,6 +1170,7 @@ def process_kwargs(self, kwargs):
(str, bool))
self.install_mode = kwargs.get('install_mode', None)
self.install_tag = stringlistify(kwargs.get('install_tag', [None]))
self.rename = kwargs.get('rename', None)
if not isinstance(self, Executable):
# build_target will always populate these as `None`, which is fine
if kwargs.get('gui_app') is not None:
Expand Down Expand Up @@ -2621,6 +2628,7 @@ def __init__(self,
feed: bool = False,
install: bool = False,
install_dir: T.Optional[T.List[T.Union[str, Literal[False]]]] = None,
rename: T.Optional[T.List[str]] = None,
install_mode: T.Optional[FileMode] = None,
install_tag: T.Optional[T.List[T.Optional[str]]] = None,
absolute_paths: bool = False,
Expand All @@ -2647,6 +2655,7 @@ def __init__(self,
self.extra_depends = list(extra_depends or [])
self.feed = feed
self.install_dir = list(install_dir or [])
self.rename = rename
self.install_mode = install_mode
self.install_tag = _process_install_tag(install_tag, len(self.outputs))
self.name = name if name else self.outputs[0]
Expand Down Expand Up @@ -2766,6 +2775,11 @@ def get_link_dep_subdirs(self) -> T.AbstractSet[str]:
def get_all_link_deps(self):
return []

def get_rename(self) -> T.List[str]:
if self.rename:
return self.rename
return [None] * len(self.outputs)

def is_internal(self) -> bool:
'''
Returns True if this is a not installed static library.
Expand Down Expand Up @@ -3093,13 +3107,13 @@ class Data(HoldableObject):
install_dir_name: str
install_mode: 'FileMode'
subproject: str
rename: T.List[str] = None
rename: T.Optional[T.List[str]] = None
install_tag: T.Optional[str] = None
data_type: str = None
follow_symlinks: T.Optional[bool] = None

def __post_init__(self) -> None:
if self.rename is None:
if not self.rename:
self.rename = [os.path.basename(f.fname) for f in self.sources]

@dataclass(eq=False)
Expand Down
13 changes: 12 additions & 1 deletion mesonbuild/interpreter/interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2056,6 +2056,7 @@ def _validate_custom_target_outputs(self, has_multi_in: bool, outputs: T.Iterabl
INSTALL_MODE_KW.evolve(since='0.47.0'),
KwargInfo('feed', bool, default=False, since='0.59.0'),
KwargInfo('capture', bool, default=False),
KwargInfo('rename', (ContainerTypeInfo(list, str), NoneType), default=None, listify=True, since='1.7.0'),
KwargInfo('console', bool, default=False, since='0.48.0'),
)
def func_custom_target(self, node: mparser.FunctionNode, args: T.Tuple[str],
Expand Down Expand Up @@ -2121,7 +2122,11 @@ def func_custom_target(self, node: mparser.FunctionNode, args: T.Tuple[str],
'or the same number of elements as the output keyword argument. '
f'(there are {len(kwargs["install_tag"])} install_tags, '
f'and {len(kwargs["output"])} outputs)')

if kwargs['rename'] and len(kwargs['rename']) != len(kwargs['output']):
raise InvalidArguments('custom_target: rename argument must either be empty or have '
'length equal to the number of outputs. '
f'(there are {len(kwargs["rename"])} rename, '
f'and {len(kwargs["output"])} outputs)')
for t in kwargs['output']:
self.validate_forbidden_targets(t)
self._validate_custom_target_outputs(len(inputs) > 1, kwargs['output'], "custom_target")
Expand All @@ -2147,6 +2152,7 @@ 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'],
rename=kwargs['rename'],
backend=self.backend)
self.add_target(tg.name, tg)
return tg
Expand Down Expand Up @@ -3482,6 +3488,11 @@ def build_target(self, node: mparser.BaseNode, args: T.Tuple[str, SourcesVarargs
target = targetclass(name, self.subdir, self.subproject, for_machine, srcs, struct, objs,
self.environment, self.compilers[for_machine], kwargs)

if kwargs['rename'] and len(kwargs['rename']) != len(target.outputs):
raise InvalidArguments('build_target: rename argument must either be empty or have '
'length equal to the number of outputs. '
f'(there are {len(kwargs["rename"])} rename, '
f'and {len(target.outputs)} outputs)')
self.add_target(name, target)
self.project_args_frozen = True
return target
Expand Down
7 changes: 7 additions & 0 deletions mesonbuild/interpreter/type_checking.py
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,13 @@ def _objects_validator(vals: T.List[ObjectTypes]) -> T.Optional[str]:
('1.1.0', 'generated sources as positional "objects" arguments')
},
),
KwargInfo(
'rename',
(ContainerTypeInfo(list, str), NoneType),
listify=True,
default=None,
since='1.7.0',
),
]


Expand Down
23 changes: 14 additions & 9 deletions mesonbuild/minstall.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,10 @@ def do_copyfile(self, from_file: str, to_file: str,
makedirs: T.Optional[T.Tuple[T.Any, str]] = None,
follow_symlinks: T.Optional[bool] = None) -> bool:
outdir = os.path.split(to_file)[0]
if os.path.basename(from_file) == os.path.basename(to_file):
outpath = outdir
else:
outpath = to_file
if not os.path.isfile(from_file) and not os.path.islink(from_file):
raise MesonException(f'Tried to install something that isn\'t a file: {from_file!r}')
# copyfile fails if the target file already exists, so remove it to
Expand All @@ -405,10 +409,10 @@ def do_copyfile(self, from_file: str, to_file: str,
append_to_log(self.lf, f'# Preserving old file {to_file}\n')
self.preserved_file_count += 1
return False
self.log(f'Installing {from_file} to {outdir}')
self.log(f'Installing {from_file} to {outpath}')
self.remove(to_file)
else:
self.log(f'Installing {from_file} to {outdir}')
self.log(f'Installing {from_file} to {outpath}')
if makedirs:
# Unpack tuple
dirmaker, outdir = makedirs
Expand Down Expand Up @@ -734,20 +738,21 @@ def install_targets(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix
raise MesonException(f'File {t.fname!r} could not be found')
file_copied = False # not set when a directory is copied
fname = check_for_stampfile(t.fname)
out_fname = t.out_fname
outdir = get_destdir_path(destdir, fullprefix, t.outdir)
outname = os.path.join(outdir, os.path.basename(fname))
final_path = os.path.join(d.prefix, t.outdir, os.path.basename(fname))
outname = os.path.join(outdir, out_fname)
final_path = os.path.join(d.prefix, t.outdir, out_fname)
should_strip = t.strip or (t.can_strip and self.options.strip)
install_rpath = t.install_rpath
install_name_mappings = t.install_name_mappings
install_mode = t.install_mode
if not os.path.exists(fname):
raise MesonException(f'File {fname!r} could not be found')
elif os.path.isfile(fname):
file_copied = self.do_copyfile(fname, outname, makedirs=(dm, outdir))
file_copied = self.do_copyfile(fname, outname, makedirs=(dm, os.path.dirname(outname)))
if should_strip and d.strip_bin is not None:
if fname.endswith('.jar'):
self.log('Not stripping jar target: {}'.format(os.path.basename(fname)))
if out_fname.endswith('.jar'):
self.log('Not stripping jar target: {}'.format(os.path.basename(out_fname)))
continue
self.do_strip(d.strip_bin, fname, outname)
if fname.endswith('.js'):
Expand All @@ -759,8 +764,8 @@ def install_targets(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix
file_copied = self.do_copyfile(wasm_source, wasm_output)
elif os.path.isdir(fname):
fname = os.path.join(d.build_dir, fname.rstrip('/'))
outname = os.path.join(outdir, os.path.basename(fname))
dm.makedirs(outdir, exist_ok=True)
outname = outname.rstrip('/')
dm.makedirs(os.path.dirname(outname), exist_ok=True)
self.do_copydir(d, fname, outname, None, install_mode, dm)
else:
raise RuntimeError(f'Unknown file type for {fname!r}')
Expand Down
4 changes: 2 additions & 2 deletions mesonbuild/mintro.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ def list_installed(installdata: backends.InstallData) -> T.Dict[str, str]:
if installdata is not None:
for t in installdata.targets:
res[os.path.join(installdata.build_dir, t.fname)] = \
os.path.join(installdata.prefix, t.outdir, os.path.basename(t.fname))
os.path.join(installdata.prefix, t.outdir, t.out_fname)
for i in installdata.data:
res[i.path] = os.path.join(installdata.prefix, i.install_path)
for i in installdata.headers:
Expand Down Expand Up @@ -233,7 +233,7 @@ def list_targets(builddata: build.Build, installdata: backends.InstallData, back
install_lookuptable = {}
for i in installdata.targets:
basename = os.path.basename(i.fname)
install_lookuptable[basename] = [str(PurePath(installdata.prefix, i.outdir, basename))]
install_lookuptable[basename] = [str(PurePath(installdata.prefix, i.outdir, i.out_fname))]
for s in installdata.symlinks:
# Symlink's target must already be in the table. They share the same list
# to support symlinks to symlinks recursively, such as .so -> .so.0 -> .so.1.2.3
Expand Down
12 changes: 12 additions & 0 deletions test cases/common/109 custom target capture/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,15 @@ if not os.path.exists(sys.argv[1]):
'''

test('capture-wrote', python3, args : ['-c', ct_output_exists, mytarget])

mytarget2 = custom_target('bindat2',
output : 'data2.dat',
input : 'data_source.txt',
rename : 'subdir2/data.dat',
capture : true,
command : [python3, comp, '@INPUT@'],
install : true,
install_dir : 'subdir'
)

test('capture-wrote2', python3, args : ['-c', ct_output_exists, mytarget2])
3 changes: 2 additions & 1 deletion test cases/common/109 custom target capture/test.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"installed": [
{"type": "file", "file": "usr/subdir/data.dat"}
{"type": "file", "file": "usr/subdir/data.dat"},
{"type": "file", "file": "usr/subdir/subdir2/data.dat"}
]
}
15 changes: 15 additions & 0 deletions test cases/common/117 shared module/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,21 @@ test('import test', e, args : m)
m2 = build_target('mymodule2', 'module.c', target_type: 'shared_module')
test('import test 2', e, args : m2)

if build_machine.system() == 'windows'
install_ext = '.dll'
elif build_machine.system() == 'darwin'
install_ext = '.dylib'
else
install_ext = '.so'
endif

# Same as above, but installed into a sub directory
m3 = shared_module('mymodule3', 'module.c',
install : true,
install_dir : join_paths(get_option('libdir'), 'modules'),
rename: 'subdir/libmymodule' + install_ext)
test('import test 3', e, args : m3)

# Shared module that does not export any symbols
shared_module('nosyms', 'nosyms.c',
override_options: ['werror=false'],
Expand Down
5 changes: 4 additions & 1 deletion test cases/common/117 shared module/test.json
Original file line number Diff line number Diff line change
Expand Up @@ -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/libmymodule?so"},
{"type": "implib", "file": "usr/lib/modules/subdir/libmymodule"},
{"type": "pdb", "file": "usr/lib/modules/subdir/mymodule"}
]
}

0 comments on commit c91e322

Please sign in to comment.