Repository: mesonbuild/meson

Issue:
install_headers(preserve_path)
```meson
# proj/kola.h -> /usr/include/myproj/kola.h
install_headers('proj/kola.h', subdir : 'myproj')

# proj/kola.h -> /usr/include/myproj/proj/kola.h
install_headers('proj/kola.h', subdir : 'myproj', preserve_path : true)
```

```meson
# you can write
install_headers(
    'foo/bar.h',
    'baz/bob.h',
    subdir : 'proj',
    preserve_path : true,
)

# instead of
install_headers('foo/bar.h', subdir : 'proj/foo')
install_headers('baz/bob.h', subdir : 'proj/baz')
```

One possible alternative to consider is `preserve_path_from : directory`, but it introduces invalid uses of API and by calling `install_headers` from subdirectory the same behavior can be achieved with proposal above, so I don't think it's worth implementing.

@textshell also mentioned:
My usecase was a list of headers to install. Where the headers might not be all in one directory. Allowing that usecase is esp. important for an IDE integration that can modify file lists, but is not smart enough to actually write and auto maintain more general elements in a meson.build file.
And i belive editing file lists should be the primary write interaction of an IDE with the meson.build files. Because meson files are really not easy to auto maintain because they allow all kinds of different styles and usages.
Also i think IDEs automatically editing target definitions to add files is not what most projects expect. I think most somewhat bigger projects have lists of files instead of a list in the target definition call.


Discussion:
Is this planned to be resolved? It's been a while since it was reported.
This is a bug, not an enhancement. This is also a duplicate of #14 I believe.

https://github.com/mesonbuild/meson/issues/14#issuecomment-514084255

Why does Meson only support a flat directory structure? Please fix this ASAP. I don't want to conform to a flat structure.
I would like to submit a PR to fix this bug.

https://github.com/mesonbuild/meson/blob/1f4023fa47803458700e52352dec51f3f85fa6c1/mesonbuild/minstall.py#L390
https://github.com/mesonbuild/meson/blob/2e41d53e4d64985d58f06235604feafa7dad5059/mesonbuild/interpreter.py#L3445
https://github.com/mesonbuild/meson/blob/2e41d53e4d64985d58f06235604feafa7dad5059/mesonbuild/interpreter.py#L2024

These files look promising. @sarum9in do you have any advice here? The only thing I am missing for the PR is finding out the directory of where the install_headers command was called. Not even sure that is the best way. I am taking suggestions. Right now, I have access to the build dir, the dest dir, and the full file path. I could probably do something like the highest shared directory of all the files will serve as the starting point of where to start mirroring directories.
Going to repeat this. I would like to submit a PR, but I need guidance. Do I need to take this to the mailing list?
I'd say the best way for implementation guidance questions is IRC and (sometimes) in the issues discussing the feature.

But you actually found some of the relevant places already. You would have to look into `self.source_strings_to_files(args)` to replace it with something that returns the relative paths in addition to the file objects. (or use source_strings_to_files and additionally safe the path in a place near to the result of source_strings_to_files)

But keep in mind, as this is a new feature it needs approval by @jpakkane before a PR can be merged. While showing clean working code in a PR often makes it easier to get attention and approval for an idea, it always carries the risk of being work that does not pan out.

(I still think meson should have this.)
Note that most people keep their headers in a single directory tree and install it with [install_subdir](https://mesonbuild.com/Reference-manual.html#install_subdir). This ensures that the header layout is the same in your source tree and install prefix.
You might still want to exclude private headers. Or the project mixes sources and headers into one directory (e.g. qtcreator only works properly with that layout).

install_headers using preserve_path from a suitable directory makes it easy to keep the layout in the install prefix the same as in the source tree.
Also if that header list is generated (could be something simple as a "find", or maybe something in the IDE) it needs a lot less manual (or "badly scripted") work when a new subdirectory with includes gets added.
Maybe the way I want to do things is an antipattern. Maybe what I need to be doing is grouping related header files together into arrays. and calling multiple `install_headers` with different subdir arguments. It also make sense code wise. Install headers on a module by module basis.

Say I have

* `http-client.h`
* `http-request.h`
* `http-response.h`
* `json-value.h`
* `json-object.h`
* `json-parser.h`

Then I could have Meson code that looks like

```meson
install_headers(http_headers, install_dir: 'my_proj', subdir: 'http')
install_headers(json_headers, install_dir: 'my_proj', subdir: 'json')
```

Thoughts? Maybe if people in this thread come to an agreement, we could amend the docs to specifically call out this use case.

Edit: I now realize that is exactly what the OP proposed as a work around.

Resolving patch:
diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py
--- a/mesonbuild/interpreter/interpreter.py
+++ b/mesonbuild/interpreter/interpreter.py
@@ -2065,6 +2065,7 @@ def add_test(self, node: mparser.BaseNode, args: T.List, kwargs: T.Dict[str, T.A
     @typed_kwargs(
         'install_headers',
         KwargInfo('install_dir', (str, NoneType)),
+        KwargInfo('preserve_path', bool, default=False, since='0.63.0'),
         KwargInfo('subdir', (str, NoneType)),
         INSTALL_MODE_KW.evolve(since='0.47.0'),
     )
@@ -2078,12 +2079,25 @@ def func_install_headers(self, node: mparser.BaseNode,
                 raise InterpreterException('install_headers: cannot specify both "install_dir" and "subdir". Use only "install_dir".')
             if os.path.isabs(install_subdir):
                 mlog.deprecation('Subdir keyword must not be an absolute path. This will be a hard error in the next release.')
+        else:
+            install_subdir = ''
+
+        dirs = collections.defaultdict(list)
+        ret_headers = []
+        if kwargs['preserve_path']:
+            for file in source_files:
+                dirname = os.path.dirname(file.fname)
+                dirs[dirname].append(file)
+        else:
+            dirs[''].extend(source_files)
 
-        h = build.Headers(source_files, install_subdir, kwargs['install_dir'],
-                          kwargs['install_mode'], self.subproject)
-        self.build.headers.append(h)
+        for childdir in dirs:
+            h = build.Headers(dirs[childdir], os.path.join(install_subdir, childdir), kwargs['install_dir'],
+                              kwargs['install_mode'], self.subproject)
+            ret_headers.append(h)
+            self.build.headers.append(h)
 
-        return h
+        return ret_headers
 
     @typed_pos_args('install_man', varargs=(str, mesonlib.File))
     @typed_kwargs(


Test patch:
diff --git a/test cases/common/9 header install/child/childdir.h b/test cases/common/9 header install/child/childdir.h
new file mode 100644
--- /dev/null
+++ b/test cases/common/9 header install/child/childdir.h	
@@ -0,0 +1,3 @@
+/* This file goes, depending on the state of `preserve_path` into subdirectory of include root or into the `child` dir of the subdirectory of include root. */
+
+int childdir_func();
diff --git a/test cases/common/9 header install/meson.build b/test cases/common/9 header install/meson.build
--- a/test cases/common/9 header install/meson.build	
+++ b/test cases/common/9 header install/meson.build	
@@ -1,11 +1,12 @@
 project('header install')
 
-as_array = ['subdir.h']
+as_array = ['subdir.h', 'child/childdir.h']
 
 subdir('vanishing_subdir')
 subdir('sub')
 
 h1 = install_headers('rootdir.h')
 h2 = install_headers(as_array, subdir : 'subdir')
-h3 = install_headers(subheader)
-h4 = install_headers(disabler())
+h3 = install_headers(as_array, subdir : 'subdir', preserve_path : true)
+h4 = install_headers(subheader)
+h5 = install_headers(disabler())
diff --git a/test cases/common/9 header install/test.json b/test cases/common/9 header install/test.json
--- a/test cases/common/9 header install/test.json	
+++ b/test cases/common/9 header install/test.json	
@@ -2,6 +2,8 @@
   "installed": [
     { "type": "file", "file": "usr/include/rootdir.h"       },
     { "type": "file", "file": "usr/include/subdir/subdir.h" },
+    { "type": "file", "file": "usr/include/subdir/childdir.h" },
+    { "type": "file", "file": "usr/include/subdir/child/childdir.h" },
     { "type": "file", "file": "usr/include/vanished.h"      },
     { "type": "file", "file": "usr/include/fileheader.h"    }
   ]