Introduce `fs.read` to read a file as a string

Following #7890, this patch introduces the ability to read the contents
of a file to the fs module.

This patch introduces the ability to read files at configure time, but
has some restrictions:
    - binary files are not supported (I don't think this will prove a
    problem, and if people are wanting to do something with binary
    files, they should probably be shelling out to their own script).
    - Only files outside the build directory allowed. This limitation
      should prevent build loops.
Given that reading an arbitrary file at configure time can affect the
configuration in almost arbitrary ways, meson should force a reconfigure
when the given file changes. This is non-configurable, but this can
easily be changed with a future keyword argument.
pull/8303/head
Luke Drummond 4 years ago committed by Jussi Pakkanen
parent 95c0790711
commit 46e3480f7c
  1. 11
      docs/markdown/Fs-module.md
  2. 17
      docs/markdown/howtox.md
  3. 40
      docs/markdown/snippets/fs_read.md
  4. 2
      mesonbuild/interpreter.py
  5. 65
      mesonbuild/modules/fs.py
  6. 1
      test cases/common/241 get_file_contents/.gitattributes
  7. 1
      test cases/common/241 get_file_contents/VERSION
  8. 21
      test cases/common/241 get_file_contents/meson.build
  9. 3
      test cases/common/241 get_file_contents/other/meson.build
  10. BIN
      test cases/common/241 get_file_contents/utf-16-text

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

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

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

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

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

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

@ -0,0 +1,3 @@
fs = import('fs')
assert(fs.read(version_file).strip() == '1.2.0')
assert(fs.read('../VERSION').strip() == '1.2.0')
Loading…
Cancel
Save