aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/markdown/Fs-module.md11
-rw-r--r--docs/markdown/howtox.md17
-rw-r--r--docs/markdown/snippets/fs_read.md40
-rw-r--r--mesonbuild/interpreter.py2
-rw-r--r--mesonbuild/modules/fs.py65
-rw-r--r--test cases/common/241 get_file_contents/.gitattributes1
-rw-r--r--test cases/common/241 get_file_contents/VERSION1
-rw-r--r--test cases/common/241 get_file_contents/meson.build21
-rw-r--r--test cases/common/241 get_file_contents/other/meson.build3
-rw-r--r--test cases/common/241 get_file_contents/utf-16-textbin0 -> 150 bytes
10 files changed, 159 insertions, 2 deletions
diff --git a/docs/markdown/Fs-module.md b/docs/markdown/Fs-module.md
index d4945e9..df9f305 100644
--- a/docs/markdown/Fs-module.md
+++ b/docs/markdown/Fs-module.md
@@ -199,3 +199,14 @@ suffix
fs.stem('foo/bar/baz.dll') # baz
fs.stem('foo/bar/baz.dll.a') # baz.dll
```
+
+### read
+- `read(path, encoding: 'utf-8')` *(since 0.57.0)*:
+ return a [string](Syntax.md#strings) with the contents of the given `path`.
+ If the `encoding` keyword argument is not specified, the file specified by
+ `path` is assumed to be utf-8 encoded. Binary files are not supported. The
+ provided paths should be relative to the current `meson.current_source_dir()`
+ or an absolute path outside the build directory is accepted. If the file
+ 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.
diff --git a/docs/markdown/howtox.md b/docs/markdown/howtox.md
index f05f3e8..8c8c0c0 100644
--- a/docs/markdown/howtox.md
+++ b/docs/markdown/howtox.md
@@ -139,6 +139,23 @@ cdata.set('SOMETHING', txt)
configure_file(...)
```
+## Generate configuration data from files
+
+`The [fs module](#Fs-modules) offers the `read` function` which enables adding
+the contents of arbitrary files to configuration data (among other uses):
+
+```meson
+fs = import('fs')
+cdata = configuration_data()
+copyright = fs.read('LICENSE')
+cdata.set('COPYRIGHT', copyright)
+if build_machine.system() == 'linux'
+ os_release = fs.read('/etc/os-release')
+ cdata.set('LINUX_BUILDER', os_release)
+endif
+configure_file(...)
+```
+
## Generate a runnable script with `configure_file`
`configure_file` preserves metadata so if your template file has
diff --git a/docs/markdown/snippets/fs_read.md b/docs/markdown/snippets/fs_read.md
new file mode 100644
index 0000000..05c215a
--- /dev/null
+++ b/docs/markdown/snippets/fs_read.md
@@ -0,0 +1,40 @@
+## Support for reading files at configuration time with the `fs` module
+
+Reading text files during configuration is now supported. This can be done at
+any time after `project` has been called
+
+```meson
+project(myproject', 'c')
+license_text = run_command(
+ find_program('python3'), '-c', 'print(open("COPYING").read())'
+).stdout().strip()
+about_header = configuration_data()
+about_header.add('COPYRIGHT', license_text)
+about_header.add('ABOUT_STRING', meson.project_name())
+...
+```
+
+There are several problems with the above approach:
+1. It's ugly and confusing
+2. If `COPYING` changes after configuration, Meson won't correctly rebuild when
+ configuration data is based on the data in COPYING
+3. It has extra overhead
+
+`fs.read` replaces the above idiom thus:
+```meson
+project(myproject', 'c')
+fs = import('fs')
+license_text = fs.read('COPYING').strip()
+about_header = configuration_data()
+about_header.add('COPYRIGHT', license_text)
+about_header.add('ABOUT_STRING', meson.project_name())
+...
+```
+
+They are not equivalent, though. Files read with `fs.read` create a
+configuration dependency on the file, and so if the `COPYING` file is modified,
+Meson will automatically reconfigure, guaranteeing the build is consistent. It
+can be used for any properly encoded text files. It supports specification of
+non utf-8 encodings too, so if you're stuck with text files in a different
+encoding, it can be passed as an argument. See the [`meson`
+object](Reference-manual.md#meson-object) documentation for details.
diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py
index 0ce0fe8..c9b6e9a 100644
--- a/mesonbuild/interpreter.py
+++ b/mesonbuild/interpreter.py
@@ -2617,7 +2617,7 @@ class Interpreter(InterpreterBase):
def get_build_def_files(self) -> T.List[str]:
return self.build_def_files
- def add_build_def_file(self, f):
+ def add_build_def_file(self, f: mesonlib.FileOrString) -> None:
# Use relative path for files within source directory, and absolute path
# for system files. Skip files within build directory. Also skip not regular
# files (e.g. /dev/stdout) Normalize the path to avoid duplicates, this
diff --git a/mesonbuild/modules/fs.py b/mesonbuild/modules/fs.py
index 2ff256b..44986f8 100644
--- a/mesonbuild/modules/fs.py
+++ b/mesonbuild/modules/fs.py
@@ -14,18 +14,25 @@
import typing as T
import hashlib
+import os
from pathlib import Path, PurePath, PureWindowsPath
from .. import mlog
from . import ExtensionModule
from . import ModuleReturnValue
-from ..mesonlib import MesonException
+from ..mesonlib import (
+ File,
+ FileOrString,
+ MesonException,
+ path_is_in_root,
+)
from ..interpreterbase import FeatureNew
from ..interpreterbase import stringArgs, noKwargs
if T.TYPE_CHECKING:
from ..interpreter import Interpreter, ModuleState
+
class FSModule(ExtensionModule):
def __init__(self, interpreter: 'Interpreter') -> None:
@@ -193,5 +200,61 @@ class FSModule(ExtensionModule):
new = original.stem
return ModuleReturnValue(str(new), [])
+ #@permittedKwargs({'encoding'})
+ @FeatureNew('fs.read', '0.57.0')
+ def read(
+ self,
+ state: 'ModuleState',
+ args: T.Sequence['FileOrString'],
+ kwargs: T.Dict[str, T.Any]
+ ) -> ModuleReturnValue:
+ '''
+ Read a file from the source tree and return its value as a decoded
+ string. If the encoding is not specified, the file is assumed to be
+ utf-8 encoded. Paths must be relative by default (to prevent accidents)
+ and are forbidden to be read from the build directory (to prevent build
+ loops)
+ '''
+ if len(args) != 1:
+ raise MesonException('expected single positional <path> arg')
+
+ path = args[0]
+ if not isinstance(path, (str, File)):
+ raise MesonException(
+ '<path> positional argument must be string or files() object')
+
+ encoding = kwargs.get('encoding', 'utf-8')
+ if not isinstance(encoding, str):
+ raise MesonException('`encoding` parameter must be a string')
+
+ src_dir = self.interpreter.environment.source_dir
+ sub_dir = self.interpreter.subdir
+ build_dir = self.interpreter.environment.get_build_dir()
+
+ if isinstance(path, File):
+ if path.is_built:
+ raise MesonException(
+ 'fs.read_file does not accept built files() objects')
+ path = os.path.join(src_dir, path.relative_name())
+ else:
+ if sub_dir:
+ src_dir = os.path.join(src_dir, sub_dir)
+ path = os.path.join(src_dir, path)
+
+ path = os.path.abspath(path)
+ if path_is_in_root(Path(path), Path(build_dir), resolve=True):
+ raise MesonException('path must not be in the build tree')
+ try:
+ with open(path, 'r', encoding=encoding) as f:
+ data = f.read()
+ except UnicodeDecodeError:
+ raise MesonException(f'decoding failed for {path}')
+ # Reconfigure when this file changes as it can contain data used by any
+ # part of the build configuration (e.g. `project(..., version:
+ # fs.read_file('VERSION')` or `configure_file(...)`
+ self.interpreter.add_build_def_file(path)
+ return ModuleReturnValue(data, [])
+
+
def initialize(*args: T.Any, **kwargs: T.Any) -> FSModule:
return FSModule(*args, **kwargs)
diff --git a/test cases/common/241 get_file_contents/.gitattributes b/test cases/common/241 get_file_contents/.gitattributes
new file mode 100644
index 0000000..abec47d
--- /dev/null
+++ b/test cases/common/241 get_file_contents/.gitattributes
@@ -0,0 +1 @@
+utf-16-text binary
diff --git a/test cases/common/241 get_file_contents/VERSION b/test cases/common/241 get_file_contents/VERSION
new file mode 100644
index 0000000..26aaba0
--- /dev/null
+++ b/test cases/common/241 get_file_contents/VERSION
@@ -0,0 +1 @@
+1.2.0
diff --git a/test cases/common/241 get_file_contents/meson.build b/test cases/common/241 get_file_contents/meson.build
new file mode 100644
index 0000000..a8c68d6
--- /dev/null
+++ b/test cases/common/241 get_file_contents/meson.build
@@ -0,0 +1,21 @@
+project(
+ 'meson-fs-read-file',
+ [],
+ version: files('VERSION')
+)
+fs = import('fs')
+
+assert(fs.read('VERSION').strip() == meson.project_version(), 'file misread')
+
+expected = (
+ '∮ E⋅da = Q, n → ∞, ∑ f(i) = ∏ g(i), ∀x∈ℝ: ⌈x⌉ = −⌊−x⌋, α ∧ ¬β = ¬(¬α ∨ β)'
+)
+assert(
+ fs.read('utf-16-text', encoding: 'utf-16').strip() == expected,
+ 'file was not decoded correctly'
+)
+
+# Make sure we handle `files()` objects properly, too
+version_file = files('VERSION')
+
+subdir('other')
diff --git a/test cases/common/241 get_file_contents/other/meson.build b/test cases/common/241 get_file_contents/other/meson.build
new file mode 100644
index 0000000..9a7e4be
--- /dev/null
+++ b/test cases/common/241 get_file_contents/other/meson.build
@@ -0,0 +1,3 @@
+fs = import('fs')
+assert(fs.read(version_file).strip() == '1.2.0')
+assert(fs.read('../VERSION').strip() == '1.2.0')
diff --git a/test cases/common/241 get_file_contents/utf-16-text b/test cases/common/241 get_file_contents/utf-16-text
new file mode 100644
index 0000000..ed1fefe
--- /dev/null
+++ b/test cases/common/241 get_file_contents/utf-16-text
Binary files differ