Repository: mesonbuild/meson

Issue:
Chaining configure_file() can cause ninja to go in an infinite loop
```meson
cfg1 = configure_file(
    ...
)
cfg2 = configure_file(
    input: cfg1,
    ...
)
```

Currently this is not possible, because cfg1 is saved in builddir and absolute path of the cfg1 File object  is trashed -
 https://github.com/mesonbuild/meson/blob/7882549f860730dda43fcc825b5beb75b0209410/mesonbuild/interpreter.py#L2338,  thus cfg2 is looking for cfg1 in the sourcedir.


Discussion:
Why do you need to do this? What is the specific use case?
I need to pre-process a file with a `command: ...`, before running it through `configuration: ...`. My current workaround is to build the cfg1 path manually. The behaviour `configure_file` has currently is really unexpected. And because it does the stripping with any File object, an example with a wider use case would probably be:

```meson
file = files('/some/absolute/path')
cfg2 = configure_file(
    input: file,
    ...
)
```
This is supposed to work already. We even have a test for it: https://github.com/mesonbuild/meson/blob/master/test%20cases/common/146%20C%20and%20CPP%20link/meson.build#L38

What exactly are you trying to do and how is it failing?
```meson
project('hello')

cfg1_output = configure_file(
    input: 'cfg1_input',
    output: 'cfg1_output',
    command : [ find_program('touch'), '@OUTPUT@' ],
)
cfg2 = configure_file(
    input: cfg1_output,
    output: 'cfg2_output',
    configuration: configuration_data(), # this sends ninja into an infinity loop
    #command : [ find_program('touch'), '@OUTPUT@' ], # this is tested in your test suite and really works 
)
```
I can reproduce the infinite loop, yes.

Resolving patch:
diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py
--- a/mesonbuild/interpreter.py
+++ b/mesonbuild/interpreter.py
@@ -2549,12 +2549,10 @@ def func_configure_file(self, node, args, kwargs):
             if not isinstance(inputfile, (str, mesonlib.File)):
                 raise InterpreterException('Input must be a string or a file')
             if isinstance(inputfile, str):
-                inputfile = os.path.join(self.subdir, inputfile)
-                ifile_abs = os.path.join(self.environment.source_dir, inputfile)
-            else:
-                ifile_abs = inputfile.absolute_path(self.environment.source_dir,
-                                                    self.environment.build_dir)
-                inputfile = inputfile.relative_name()
+                inputfile = mesonlib.File.from_source_file(self.environment.source_dir,
+                                                           self.subdir, inputfile)
+            ifile_abs = inputfile.absolute_path(self.environment.source_dir,
+                                                self.environment.build_dir)
         elif 'command' in kwargs and '@INPUT@' in kwargs['command']:
             raise InterpreterException('@INPUT@ used as command argument, but no input file specified.')
         # Validate output
@@ -2575,18 +2573,13 @@ def func_configure_file(self, node, args, kwargs):
                 raise InterpreterException('Argument "configuration" is not of type configuration_data')
             mlog.log('Configuring', mlog.bold(output), 'using configuration')
             if inputfile is not None:
-                # Normalize the path of the conffile to avoid duplicates
-                # This is especially important to convert '/' to '\' on Windows
-                conffile = os.path.normpath(inputfile)
-                if conffile not in self.build_def_files:
-                    self.build_def_files.append(conffile)
                 os.makedirs(os.path.join(self.environment.build_dir, self.subdir), exist_ok=True)
                 missing_variables = mesonlib.do_conf_file(ifile_abs, ofile_abs,
                                                           conf.held_object)
                 if missing_variables:
                     var_list = ", ".join(map(repr, sorted(missing_variables)))
                     mlog.warning(
-                        "The variable(s) %s in the input file %r are not "
+                        "The variable(s) %s in the input file %s are not "
                         "present in the given configuration data" % (
                             var_list, inputfile))
             else:
@@ -2616,6 +2609,17 @@ def func_configure_file(self, node, args, kwargs):
                 mesonlib.replace_if_different(ofile_abs, dst_tmp)
         else:
             raise InterpreterException('Configure_file must have either "configuration" or "command".')
+        # If the input is a source file, add it to the list of files that we
+        # need to reconfigure on when they change. FIXME: Do the same for
+        # files() objects in the command: kwarg.
+        if inputfile and not inputfile.is_built:
+            # Normalize the path of the conffile (relative to the
+            # source root) to avoid duplicates. This is especially
+            # important to convert '/' to '\' on Windows
+            conffile = os.path.normpath(inputfile.relative_name())
+            if conffile not in self.build_def_files:
+                self.build_def_files.append(conffile)
+        # Install file if requested
         idir = kwargs.get('install_dir', None)
         if isinstance(idir, str):
             cfile = mesonlib.File.from_built_file(ofile_path, ofile_fname)


Test patch:
diff --git a/test cases/common/114 multiple dir configure file/meson.build b/test cases/common/114 multiple dir configure file/meson.build
--- a/test cases/common/114 multiple dir configure file/meson.build	
+++ b/test cases/common/114 multiple dir configure file/meson.build	
@@ -5,3 +5,7 @@ subdir('subdir')
 configure_file(input : 'subdir/someinput.in',
   output : 'outputhere',
   configuration : configuration_data())
+
+configure_file(input : cfile1,
+               output : '@BASENAME@',
+               configuration : configuration_data())
diff --git a/test cases/common/114 multiple dir configure file/subdir/foo.txt b/test cases/common/114 multiple dir configure file/subdir/foo.txt
new file mode 100644
diff --git a/test cases/common/114 multiple dir configure file/subdir/meson.build b/test cases/common/114 multiple dir configure file/subdir/meson.build
--- a/test cases/common/114 multiple dir configure file/subdir/meson.build	
+++ b/test cases/common/114 multiple dir configure file/subdir/meson.build	
@@ -2,3 +2,10 @@ configure_file(input : 'someinput.in',
   output : 'outputsubdir',
   install : false,
   configuration : configuration_data())
+
+py3 = import('python3').find_python()
+
+cfile1 = configure_file(input : 'foo.txt',
+                        output : 'foo.h.in',
+                        capture : true,
+                        command : [py3, '-c', 'print("#mesondefine FOO_BAR")'])