diff --git a/docs/markdown/Fs-module.md b/docs/markdown/Fs-module.md index 663aba41bba4..139355192456 100644 --- a/docs/markdown/Fs-module.md +++ b/docs/markdown/Fs-module.md @@ -215,3 +215,30 @@ fs.stem('foo/bar/baz.dll.a') # baz.dll specified by `path` changes, this will trigger Meson to reconfigure the project. If the file specified by `path` is a `files()` object it cannot refer to a built file. + + +### copyfile + +*Since 0.64.0* + +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 + +Has the following optional arguments: + - dest `str`: the name of the output file. If unset will be the basename of + the src argument + +Has the following keyword arguments: + - install `bool`: Whether to install the copied file, defaults to false + - install_dir `str`: Where to install the file to + - install_tag: `str`: the install tag to assign to this target + - install_mode `array[str | int]`: the mode to install the file with + +returns: + - a [[custom_target]] object + +```meson +copy = fs.copyfile('input-file', 'output-file') +``` diff --git a/docs/markdown/snippets/fs_copyfile.md b/docs/markdown/snippets/fs_copyfile.md new file mode 100644 index 000000000000..dfb5d8985f77 --- /dev/null +++ b/docs/markdown/snippets/fs_copyfile.md @@ -0,0 +1,17 @@ +## `fs.copyfile` to replace `configure_file(copy : true)` + +A new method has been added to the `fs` module, `copyfile`. This method replaces +`configure_file(copy : true)`, but only copies files. Unlike `configure_file()` +it runs at build time, and the output name is optional defaulting to the +filename without paths of the input if unset: + +```meson +fs.copyfile('src/file.txt') +``` +Will create a file in the current build directory called `file.txt` + + +```meson +fs.copyfile('file.txt', 'outfile.txt') +``` +Will create a copy renamed to `outfile.txt` diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index 5752d4c5a655..b904055c844c 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -2453,7 +2453,10 @@ def func_install_subdir(self, node: mparser.BaseNode, args: T.Tuple[str], 'configuration', (ContainerTypeInfo(dict, (str, int, bool)), build.ConfigurationData, NoneType), ), - KwargInfo('copy', bool, default=False, since='0.47.0'), + KwargInfo( + 'copy', bool, default=False, since='0.47.0', + deprecated='0.64.0', deprecated_message='Use fs.copy instead', + ), KwargInfo('encoding', str, default='utf-8', since='0.47.0'), KwargInfo('format', str, default='meson', since='0.46.0', validator=in_set_validator({'meson', 'cmake', 'cmake@'})), diff --git a/mesonbuild/modules/fs.py b/mesonbuild/modules/fs.py index f0e3f4c885a2..7e3c75a11bbc 100644 --- a/mesonbuild/modules/fs.py +++ b/mesonbuild/modules/fs.py @@ -12,24 +12,28 @@ # See the License for the specific language governing permissions and # limitations under the License. -import typing as T +from __future__ import annotations +from pathlib import Path, PurePath, PureWindowsPath import hashlib import os -from pathlib import Path, PurePath, PureWindowsPath +import typing as T +from . import ExtensionModule, ModuleReturnValue, ModuleInfo from .. import mlog -from . import ExtensionModule, ModuleInfo +from ..build import CustomTarget, InvalidArguments +from ..interpreter.type_checking import INSTALL_KW, INSTALL_MODE_KW, INSTALL_TAG_KW, NoneType +from ..interpreterbase import FeatureNew, KwargInfo, typed_kwargs, typed_pos_args, noKwargs from ..mesonlib import ( File, MesonException, + has_path_sep, path_is_in_root, ) -from ..interpreterbase import FeatureNew, KwargInfo, typed_kwargs, typed_pos_args, noKwargs if T.TYPE_CHECKING: from . import ModuleState from ..interpreter import Interpreter - from ..mesonlib import FileOrString + from ..mesonlib import FileOrString, FileMode from typing_extensions import TypedDict @@ -38,6 +42,15 @@ class ReadKwArgs(TypedDict): encoding: str + class CopyKw(TypedDict): + + """Kwargs for fs.copy""" + + install: bool + install_dir: T.Optional[str] + install_mode: FileMode + install_tag: T.Optional[str] + class FSModule(ExtensionModule): @@ -61,6 +74,7 @@ def __init__(self, interpreter: 'Interpreter') -> None: 'name': self.name, 'stem': self.stem, 'read': self.read, + 'copyfile': self.copyfile, }) def _absolute_dir(self, state: 'ModuleState', arg: 'FileOrString') -> Path: @@ -255,6 +269,47 @@ def read(self, state: 'ModuleState', args: T.Tuple['FileOrString'], kwargs: 'Rea self.interpreter.add_build_def_file(path) return data + @FeatureNew('fs.copyfile', '0.64.0') + @typed_pos_args('fs.copyfile', (File, str), optargs=[str]) + @typed_kwargs( + 'fs.copyfile', + INSTALL_KW, + INSTALL_MODE_KW, + INSTALL_TAG_KW, + KwargInfo('install_dir', (str, NoneType)), + ) + def copyfile(self, state: ModuleState, args: T.Tuple[FileOrString, 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] + + # 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) + if has_path_sep(dest): + raise InvalidArguments('Destination path may not have path separators') + + ct = CustomTarget( + dest, + state.subdir, + state.subproject, + state.environment, + state.environment.get_build_command() + ['--internal', 'copy', '@INPUT@', '@OUTPUT@'], + [src], + [dest], + build_by_default=True, + install=kwargs['install'], + install_dir=kwargs['install_dir'], + install_mode=kwargs['install_mode'], + install_tag=kwargs['install_tag'], + backend=state.backend, + ) + + return ModuleReturnValue(ct, [ct]) + def initialize(*args: T.Any, **kwargs: T.Any) -> FSModule: return FSModule(*args, **kwargs) diff --git a/test cases/common/14 configure file/meson.build b/test cases/common/14 configure file/meson.build index 91a56ff62fa1..1dd67342647c 100644 --- a/test cases/common/14 configure file/meson.build +++ b/test cases/common/14 configure file/meson.build @@ -1,4 +1,6 @@ -project('configure file test', 'c', meson_version: '>=0.47.0') +project('configure file test', 'c', meson_version: '>=0.63.0') + +fs = import('fs') conf = configuration_data() @@ -188,6 +190,9 @@ if ret.returncode() != 0 endif # Now the same, but using a File object as an argument. inf2 = files('invalid-utf8.bin.in')[0] +outf = configure_file(input : inf2, + output : 'invalid-utf8.bin', + copy: true) ret = run_command(check_file, inf2, outf, check: false) if ret.returncode() != 0 error('Error running command: @0@\n@1@'.format(ret.stdout(), ret.stderr())) @@ -202,6 +207,21 @@ if ret.returncode() != 0 error('Error running command: @0@\n@1@'.format(ret.stdout(), ret.stderr())) endif +# Test the fs replacement +# Test copying of an empty configuration data object +inf = 'invalid-utf8.bin.in' +outf = fs.copyfile(inf, 'invalid-utf8-1.bin') +test('fs.copyfile string', check_file, args: [files(inf), outf]) + +# Test with default outname of string +outf = fs.copyfile(inf) +test('fs.copyfile default name', check_file, args: [files(inf), outf]) + +# Now the same, but using a File object as an argument. +inf2 = files('invalid-utf8.bin.in')[0] +outf = fs.copyfile(inf2, 'invalid-utf8-2.bin') +test('fs.copyfile file', check_file, args: [inf2, outf]) + # Test non isolatin1 encoded input file which fails to decode with utf-8 conf8 = configuration_data() conf8.set('var', 'foo')