From 74faddbb44475b0671fd1acbaedace2e8a596263 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Lureau?= Date: Wed, 22 Apr 2026 12:02:24 +0400 Subject: [PATCH] modules: fs: accept build targets as fs.copyfile() source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allow CustomTarget, CustomTargetIndex, and BuildTarget as the first argument to fs.copyfile(), so that build artifacts can be copied without resorting to a manual custom_target. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Marc-André Lureau --- docs/markdown/Fs-module.md | 4 +++- .../snippets/fs-copyfile-build-targets.md | 5 ++++ mesonbuild/modules/fs.py | 23 ++++++++++++++----- test cases/common/220 fs module/meson.build | 13 +++++++++++ 4 files changed, 38 insertions(+), 7 deletions(-) create mode 100644 docs/markdown/snippets/fs-copyfile-build-targets.md diff --git a/docs/markdown/Fs-module.md b/docs/markdown/Fs-module.md index abff9bcad..7ca854ea1 100644 --- a/docs/markdown/Fs-module.md +++ b/docs/markdown/Fs-module.md @@ -259,7 +259,9 @@ returns: Copy a file from the source directory to the build directory at build time. Has the following positional arguments: - - src `File | str`: the file to copy + - src `File | str | build_tgt | custom_tgt | custom_idx`: the file to copy. + *Since 1.12.0* build targets, custom targets, and custom target indices are + also accepted. Has the following optional arguments: - dest `str`: the name of the output file. If unset will be the basename of diff --git a/docs/markdown/snippets/fs-copyfile-build-targets.md b/docs/markdown/snippets/fs-copyfile-build-targets.md new file mode 100644 index 000000000..284a860fc --- /dev/null +++ b/docs/markdown/snippets/fs-copyfile-build-targets.md @@ -0,0 +1,5 @@ +## `fs.copyfile()` now accepts build targets as input + +`fs.copyfile()` now accepts `build_tgt`, `custom_tgt`, and `custom_idx` +as the source argument, in addition to `File` and `str`. This makes it +possible to copy build artifacts without resorting to a `custom_target`. diff --git a/mesonbuild/modules/fs.py b/mesonbuild/modules/fs.py index ba7449026..d6a266f2f 100644 --- a/mesonbuild/modules/fs.py +++ b/mesonbuild/modules/fs.py @@ -280,7 +280,7 @@ class FSModule(ExtensionModule): return data @FeatureNew('fs.copyfile', '0.64.0') - @typed_pos_args('fs.copyfile', (File, str), optargs=[str]) + @typed_pos_args('fs.copyfile', (File, str, CustomTarget, CustomTargetIndex, BuildTarget), optargs=[str]) @typed_kwargs( 'fs.copyfile', INSTALL_KW, @@ -289,17 +289,28 @@ class FSModule(ExtensionModule): KwargInfo('install_dir', (str, NoneType)), BUILD_SUBDIR_KW.evolve(since='1.12.0'), ) - def copyfile(self, state: ModuleState, args: T.Tuple[FileOrString, T.Optional[str]], + def copyfile(self, state: ModuleState, args: T.Tuple[T.Union[FileOrString, BuildTargetTypes], T.Optional[str]], kwargs: CopyKw) -> ModuleReturnValue: """Copy a file into the build directory at build time.""" if kwargs['install'] and not kwargs['install_dir']: raise InvalidArguments('"install_dir" must be specified when "install" is true') - src = self.interpreter.source_strings_to_files([args[0]])[0] + src: T.Union[File, BuildTarget, CustomTarget, CustomTargetIndex] + if isinstance(args[0], (CustomTarget, CustomTargetIndex, BuildTarget)): + FeatureNew.single_use('fs.copyfile with build_tgt, custom_tgt, and custom_idx', '1.12.0', state.subproject, location=state.current_node) + if isinstance(args[0], CustomTarget) and len(args[0].get_outputs()) > 1: + raise InvalidArguments( + f'custom_target {args[0].name!r} has more than one output! ' + f'Use `{args[0].name}[]` to select the output to copy.') + src = args[0] + default_name = src.get_filename() + else: + src = self.interpreter.source_strings_to_files([args[0]])[0] + # The input is allowed to have path separators, but the output may not, + # so use the basename for the default case + default_name = os.path.basename(src.fname) - # The input is allowed to have path separators, but the output may not, - # so use the basename for the default case - dest = args[1] if args[1] else os.path.basename(src.fname) + dest = args[1] if args[1] else default_name if has_path_sep(dest): raise InvalidArguments('Destination path may not have path separators') diff --git a/test cases/common/220 fs module/meson.build b/test cases/common/220 fs module/meson.build index 383b263ab..7492854b0 100644 --- a/test cases/common/220 fs module/meson.build +++ b/test cases/common/220 fs module/meson.build @@ -7,6 +7,9 @@ fs = import('fs') f = files('meson.build') btgt = executable('btgt', 'btgt.c') ctgt = fs.copyfile('ctgt.txt') +ctgt_copy = fs.copyfile(ctgt, 'ctgt_copy.txt') +ctgt_idx_copy = fs.copyfile(ctgt[0], 'ctgt_idx_copy.txt') +btgt_copy = fs.copyfile(btgt, 'btgt_copy') assert(fs.exists('meson.build'), 'Existing file reported as missing.') assert(not fs.exists('nonexisting'), 'Nonexisting file was found.') @@ -199,3 +202,13 @@ subproject('subbie') testcase expect_error('File notfound does not exist.') fs.read('notfound') endtestcase + +multi_out = custom_target('multi_out', + output: ['a.txt', 'b.txt'], + command: [find_program('cp'), '@INPUT@', '@OUTPUT@'], + input: 'meson.build', +) + +testcase expect_error('custom_target \'multi_out\' has more than one output! Use `multi_out[]` to select the output to copy.') + fs.copyfile(multi_out) +endtestcase