aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarc-André Lureau <marcandre.lureau@redhat.com>2019-07-27 17:25:38 +0400
committerJussi Pakkanen <jpakkane@gmail.com>2019-09-30 22:17:50 +0300
commit01569fee2e8130b3ac54659c119e73180d3dafee (patch)
tree7fc3b94f3aa921e548455caca7f118ad42b0f677
parente32b0f8fbbd16b6cdd795a9a596eb56813620055 (diff)
downloadmeson-01569fee2e8130b3ac54659c119e73180d3dafee.zip
meson-01569fee2e8130b3ac54659c119e73180d3dafee.tar.gz
meson-01569fee2e8130b3ac54659c119e73180d3dafee.tar.bz2
Add depfile to configure_file()
In qemu, minikconf generates a depfile that meson could use to automatically reconfigure on dependency change. Note: someone clever can perhaps find a way to express this with a ninja rule & depfile=. I didn't manage, so I wrote a simple depfile parser.
-rw-r--r--docs/markdown/Reference-manual.md3
-rw-r--r--docs/markdown/snippets/configure_file_enhancements.md3
-rw-r--r--mesonbuild/depfile.py85
-rw-r--r--mesonbuild/interpreter.py24
-rwxr-xr-xrun_unittests.py29
-rw-r--r--test cases/common/14 configure file/depfile0
-rwxr-xr-xtest cases/common/14 configure file/generator-deps.py19
-rw-r--r--test cases/common/14 configure file/meson.build11
8 files changed, 174 insertions, 0 deletions
diff --git a/docs/markdown/Reference-manual.md b/docs/markdown/Reference-manual.md
index 6009d30..176cde4 100644
--- a/docs/markdown/Reference-manual.md
+++ b/docs/markdown/Reference-manual.md
@@ -260,6 +260,9 @@ These are all the supported keyword arguments:
substitutions.
- `copy` *(added 0.47.0)* as explained above, if specified Meson only
copies the file from input to output.
+- `depfile` *(added 0.52.0)* is a dependency file that the command can write listing
+ all the additional files this target depends on. A change
+ in any one of these files triggers a reconfiguration.
- `format` *(added 0.46.0)* the format of defines. It defaults to `meson`, and so substitutes
`#mesondefine` statements and variables surrounded by `@` characters, you can also use `cmake`
to replace `#cmakedefine` statements and variables with the `${variable}` syntax. Finally you can use
diff --git a/docs/markdown/snippets/configure_file_enhancements.md b/docs/markdown/snippets/configure_file_enhancements.md
index 35a64b4..7fee7b2 100644
--- a/docs/markdown/snippets/configure_file_enhancements.md
+++ b/docs/markdown/snippets/configure_file_enhancements.md
@@ -1,3 +1,6 @@
## Enhancements to `configure_file()`
`input:` now accepts multiple input file names for `command:`-configured file.
+
+`depfile:` keyword argument is now accepted. The dependency file can
+list all the additional files the configure target depends on.
diff --git a/mesonbuild/depfile.py b/mesonbuild/depfile.py
new file mode 100644
index 0000000..7a896cd
--- /dev/null
+++ b/mesonbuild/depfile.py
@@ -0,0 +1,85 @@
+# Copyright 2019 Red Hat, Inc.
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import collections
+
+def parse(lines):
+ rules = []
+ targets = []
+ deps = []
+ in_deps = False
+ out = ''
+ for line in lines:
+ if not line.endswith('\n'):
+ line += '\n'
+ escape = None
+ for c in line:
+ if escape:
+ if escape == '$' and c != '$':
+ out += '$'
+ if escape == '\\' and c == '\n':
+ continue
+ out += c
+ escape = None
+ continue
+ if c == '\\' or c == '$':
+ escape = c
+ continue
+ elif c in (' ', '\n'):
+ if out != '':
+ if in_deps:
+ deps.append(out)
+ else:
+ targets.append(out)
+ out = ''
+ if c == '\n':
+ rules.append((targets, deps))
+ targets = []
+ deps = []
+ in_deps = False
+ continue
+ elif c == ':':
+ targets.append(out)
+ out = ''
+ in_deps = True
+ continue
+ out += c
+ return rules
+
+Target = collections.namedtuple('Target', ['deps'])
+
+class DepFile:
+ def __init__(self, lines):
+ rules = parse(lines)
+ depfile = {}
+ for (targets, deps) in rules:
+ for target in targets:
+ t = depfile.setdefault(target, Target(deps=set()))
+ for dep in deps:
+ t.deps.add(dep)
+ self.depfile = depfile
+
+ def get_all_dependencies(self, target, visited=None):
+ deps = set()
+ if not visited:
+ visited = set()
+ if target in visited:
+ return set()
+ visited.add(target)
+ target = self.depfile.get(target)
+ if not target:
+ return set()
+ deps.update(target.deps)
+ for dep in target.deps:
+ deps.update(self.get_all_dependencies(dep, visited))
+ return deps
diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py
index 2a77eaa..e52ceae 100644
--- a/mesonbuild/interpreter.py
+++ b/mesonbuild/interpreter.py
@@ -24,6 +24,7 @@ from . import mesonlib
from .mesonlib import FileMode, MachineChoice, Popen_safe, listify, extract_as_list, has_path_sep
from .dependencies import ExternalProgram
from .dependencies import InternalDependency, Dependency, NotFoundDependency, DependencyException
+from .depfile import DepFile
from .interpreterbase import InterpreterBase
from .interpreterbase import check_stringlist, flatten, noPosargs, noKwargs, stringArgs, permittedKwargs, noArgsFlattening
from .interpreterbase import InterpreterException, InvalidArguments, InvalidCode, SubdirDoneRequest
@@ -1960,6 +1961,7 @@ permitted_kwargs = {'add_global_arguments': {'language', 'native'},
'configuration',
'command',
'copy',
+ 'depfile',
'install_dir',
'install_mode',
'capture',
@@ -3603,6 +3605,7 @@ This will become a hard error in the future.''' % kwargs['input'], location=self
@FeatureNewKwargs('configure_file', '0.46.0', ['format'])
@FeatureNewKwargs('configure_file', '0.41.0', ['capture'])
@FeatureNewKwargs('configure_file', '0.50.0', ['install'])
+ @FeatureNewKwargs('configure_file', '0.52.0', ['depfile'])
@permittedKwargs(permitted_kwargs['configure_file'])
def func_configure_file(self, node, args, kwargs):
if len(args) > 0:
@@ -3648,6 +3651,13 @@ This will become a hard error in the future.''' % kwargs['input'], location=self
if output_format not in ('c', 'nasm'):
raise InterpreterException('"format" possible values are "c" or "nasm".')
+ if 'depfile' in kwargs:
+ depfile = kwargs['depfile']
+ if not isinstance(depfile, str):
+ raise InterpreterException('depfile file name must be a string')
+ else:
+ depfile = None
+
# Validate input
inputs = self.source_strings_to_files(extract_as_list(kwargs, 'input'))
inputs_abs = []
@@ -3665,6 +3675,8 @@ This will become a hard error in the future.''' % kwargs['input'], location=self
values = mesonlib.get_filenames_templates_dict(inputs_abs, None)
outputs = mesonlib.substitute_values([output], values)
output = outputs[0]
+ if depfile:
+ depfile = mesonlib.substitute_values([depfile], values)[0]
ofile_rpath = os.path.join(self.subdir, output)
if ofile_rpath in self.configure_file_outputs:
mesonbuildfile = os.path.join(self.subdir, 'meson.build')
@@ -3716,6 +3728,9 @@ This will become a hard error in the future.''' % kwargs['input'], location=self
# that the command is run from is 'unspecified', so it could change.
# Currently it's builddir/subdir for in_builddir else srcdir/subdir.
values = mesonlib.get_filenames_templates_dict(inputs_abs, [ofile_abs])
+ if depfile:
+ depfile = os.path.join(self.environment.get_scratch_dir(), depfile)
+ values['@DEPFILE@'] = depfile
# Substitute @INPUT@, @OUTPUT@, etc here.
cmd = mesonlib.substitute_values(kwargs['command'], values)
mlog.log('Configuring', mlog.bold(output), 'with command')
@@ -3731,6 +3746,15 @@ This will become a hard error in the future.''' % kwargs['input'], location=self
if inputs_abs:
shutil.copymode(inputs_abs[0], dst_tmp)
mesonlib.replace_if_different(ofile_abs, dst_tmp)
+ if depfile:
+ mlog.log('Reading depfile:', mlog.bold(depfile))
+ with open(depfile, 'r') as f:
+ df = DepFile(f.readlines())
+ deps = df.get_all_dependencies(ofile_fname)
+ for dep in deps:
+ if dep not in self.build_def_files:
+ self.build_def_files.append(dep)
+
elif 'copy' in kwargs:
if len(inputs_abs) != 1:
raise InterpreterException('Exactly one input file must be given in copy mode')
diff --git a/run_unittests.py b/run_unittests.py
index c85ae50..5281aa9 100755
--- a/run_unittests.py
+++ b/run_unittests.py
@@ -40,6 +40,7 @@ from pathlib import (PurePath, Path)
from distutils.dir_util import copy_tree
import mesonbuild.mlog
+import mesonbuild.depfile
import mesonbuild.compilers
import mesonbuild.environment
import mesonbuild.mesonlib
@@ -1119,6 +1120,34 @@ class InternalTests(unittest.TestCase):
self.assertEqual(quote_arg(arg), expected)
self.assertEqual(split_args(expected)[0], arg)
+ def test_depfile(self):
+ for (f, target, expdeps) in [
+ # empty, unknown target
+ ([''], 'unknown', set()),
+ # simple target & deps
+ (['meson/foo.o : foo.c foo.h'], 'meson/foo.o', set({'foo.c', 'foo.h'})),
+ (['meson/foo.o: foo.c foo.h'], 'foo.c', set()),
+ # get all deps
+ (['meson/foo.o: foo.c foo.h',
+ 'foo.c: gen.py'], 'meson/foo.o', set({'foo.c', 'foo.h', 'gen.py'})),
+ (['meson/foo.o: foo.c foo.h',
+ 'foo.c: gen.py'], 'foo.c', set({'gen.py'})),
+ # linue continuation, multiple targets
+ (['foo.o \\', 'foo.h: bar'], 'foo.h', set({'bar'})),
+ (['foo.o \\', 'foo.h: bar'], 'foo.o', set({'bar'})),
+ # \\ handling
+ (['foo: Program\\ F\\iles\\\\X'], 'foo', set({'Program Files\\X'})),
+ # $ handling
+ (['f$o.o: c/b'], 'f$o.o', set({'c/b'})),
+ (['f$$o.o: c/b'], 'f$o.o', set({'c/b'})),
+ # cycles
+ (['a: b', 'b: a'], 'a', set({'a', 'b'})),
+ (['a: b', 'b: a'], 'b', set({'a', 'b'})),
+ ]:
+ d = mesonbuild.depfile.DepFile(f)
+ deps = d.get_all_dependencies(target)
+ self.assertEqual(deps, expdeps)
+
@unittest.skipIf(is_tarball(), 'Skipping because this is a tarball release')
class DataTests(unittest.TestCase):
diff --git a/test cases/common/14 configure file/depfile b/test cases/common/14 configure file/depfile
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test cases/common/14 configure file/depfile
diff --git a/test cases/common/14 configure file/generator-deps.py b/test cases/common/14 configure file/generator-deps.py
new file mode 100755
index 0000000..376ddb2
--- /dev/null
+++ b/test cases/common/14 configure file/generator-deps.py
@@ -0,0 +1,19 @@
+#!/usr/bin/env python3
+
+import sys, os
+from pathlib import Path
+
+if len(sys.argv) != 3:
+ print("Wrong amount of parameters.")
+
+build_dir = Path(os.environ['MESON_BUILD_ROOT'])
+subdir = Path(os.environ['MESON_SUBDIR'])
+outputf = Path(sys.argv[1])
+
+with outputf.open('w') as ofile:
+ ofile.write("#define ZERO_RESULT 0\n")
+
+depf = Path(sys.argv[2])
+if not depf.exists():
+ with depf.open('w') as ofile:
+ ofile.write("{}: depfile\n".format(outputf.name))
diff --git a/test cases/common/14 configure file/meson.build b/test cases/common/14 configure file/meson.build
index d0f3d54..4a2f15a 100644
--- a/test cases/common/14 configure file/meson.build
+++ b/test cases/common/14 configure file/meson.build
@@ -57,6 +57,17 @@ if ret.returncode() != 0
error('Error running command: @0@\n@1@'.format(ret.stdout(), ret.stderr()))
endif
+genscript2deps = '@0@/generator-deps.py'.format(meson.current_source_dir())
+ofile2deps = '@0@/config2deps.h'.format(meson.current_build_dir())
+outf = configure_file(
+ output : 'config2deps.h',
+ depfile : 'depfile.d',
+ command : [genprog, genscript2deps, ofile2deps, '@DEPFILE@'])
+ret = run_command(check_file, outf)
+if ret.returncode() != 0
+ error('Error running command: @0@\n@1@'.format(ret.stdout(), ret.stderr()))
+endif
+
found_script = find_program('generator.py')
# More configure_file tests in here
subdir('subdir')