Repository: mesonbuild/meson

Issue:
install_subdir should have an option to install contents of the directory
## Feature request
It would be nice to have an option to strip subdirectory name and install only its contents.

## Use case
Imagine you have a project
```
include/
        project/
                header.hpp
meson.build
```

And you want to install all headers from `include` subdirectory in `meson.build`:
```
install_subdir('include', install_dir : get_option('includedir'))
```
which currently results in `/usr/include/include/project/header.hpp`.
This can be of course worked around by not specifying install_dir but this is not reliable
if `get_option('includedir') != 'include'`.

Another workaround is to put meson.build into include directory but this
will require modification of other build systems used for the same project if they simply copy contents
of the directory: you don't want to install `meson.build` to `/usr/include`.

## Interface proposal
Feature I would expect is something similar to
```
install_subdir('include', install_dir : get_option('includedir'), strip_dirname : true)
install_subdir('include', install_dir : get_option('includedir'), install_contents : true)
```

## Example of similar feature from CMake
If you have 2 files: `['foo/a', 'foo/b']`
```
install(DIRECTORY foo DESTINATION bar) # results in ['bar/foo/a', 'bar/foo/b']
install(DIRECTORY foo/ DESTINATION bar) # results in ['bar/a', 'bar/b']
```
CMake uses trailing `/` to decide if it installs directory itself or its contents.


Discussion:
What happens if you do:

    install_subdir('include/project', install_dir : get_option('includedir'))

It is another workaround indeed. It creates `/usr/include/project/header.hpp`. However I can always reformulate use-case into something like this: I have long list of files inside include directory itself, so that I would like to avoid listing them:
```
include/
        a.hpp
        b.hpp
        c.hpp
        d.hpp
        e.hpp
        subidr/
               f.hpp
               g.hpp
```

This is probably not high priority but pretty convenient. If you agree with the feasibility of the feature and naming convention I could take it and implement.
Another common use case for this is installing resources where you have something like this:

    resources
      img
        foo.png
      sound
        blip.ogg

And you need it installed to something like:

    /usr/share/mypackage/part1_resources
      img
        foo.png
      sound
        blip.ogg

And then in some other source directory you have also a directory called `resources` and need to install it to `/usr/share/mypackage/part1_resources`.

So yes, this would be a useful addition. We definitely want it to happen based on a keyword rather than a slash in the pathname, that one is just plain confusing. I can't think of a very descriptive name for this, though. Maybe `strip_leading_directory`?
It's not leading directory, rather trailing... Maybe `strip_directory_name`?
  
OK, so far we have options
```
install_subdir('include', install_dir : get_option('includedir'), strip_dirname : true)
install_subdir('include', install_dir : get_option('includedir'), install_contents : true)
install_subdir('include', install_dir : get_option('includedir'), strip_leading_directory: true)
install_subdir('include', install_dir : get_option('includedir'), strip_trailing_directory: true)
install_subdir('include', install_dir : get_option('includedir'), strip_directory_name: true)
```
We could also play around `merge`, aka `merge_contents`, but this might be more confusing.
I probably lean towards `install_contents` since it points users attention to contents and says that it is the most important. Since there is already `exclude_directories` present I am afraid that might be harder
to read if both options are present, see
```
install_subdir('include', install_dir : get_option('includedir'),
               strip_directory_name : true, exclude_directories : ['a', 'b', 'c'])
install_subdir('include', install_dir : get_option('includedir'),
               strip_trailing_directory: true, exclude_directories : ['a', 'b', 'c'])
install_subdir('include', install_dir : get_option('includedir'),
               install_contents: true, exclude_directories : ['a', 'b', 'c'])
```
> It's not leading directory, rather trailing... Maybe strip_directory_name

How do you mean. If you have prefix '/usr' and includedir is `include` then installing subdir `foo` with one file `bar.h` without this does:

    install_subdir('foo', ...)
      -> /usr/include/foo/bar.h

But with this keyword it should do:

    install_subdir('foo', ..., strip_leading_dir : true)
      -> /usr/include/bar.h

Thus the leading segment of the subdirectory to be installed (`foo`) is stripped from the final install location.
That's interesting way to look at this, proving that this keyword is ambiguous and should not be used. My way of reading it was this:
If you want to install directory `$project/foo/bar` into `/usr/include` you can either write
```
install_subdir('foo/baz/bar', install_dir : '/usr/include') # /usr/include/bar/contents
install_subdir('foo/baz/bar', install_dir : '/usr/include', strip_trailing_dir : true) # /usr/include/contents
```
In the 2nd option we omit last component of 'foo/baz/bar' so I called it trailing.
As I understood you were reading it as "leading part of `foo/baz/bar/contents`", which is also correct.
But to be honest both versions look ambiguous.
To reiterate, this is something that we definitely need and want. It is just a question of coming up with a good name (which is one of the hard things of computer science). How about `elide_directory`? It is a term that is both descriptive and currently unused (both in Meson and in build systems in general).
I think it's decent idea given [2nd translation of elide](https://en.oxforddictionaries.com/definition/elide). If you don't mind I will try to implement this feature, ETA 2 weeks for initial implementation.

Resolving patch:
diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py
--- a/mesonbuild/backend/ninjabackend.py
+++ b/mesonbuild/backend/ninjabackend.py
@@ -851,8 +851,9 @@ def generate_subdir_install(self, d):
                                    sd.source_subdir,
                                    sd.installable_subdir).rstrip('/')
             dst_dir = os.path.join(self.environment.get_prefix(),
-                                   sd.install_dir,
-                                   os.path.basename(src_dir))
+                                   sd.install_dir)
+            if not sd.strip_directory:
+                dst_dir = os.path.join(dst_dir, os.path.basename(src_dir))
             d.install_subdirs.append([src_dir, dst_dir, sd.install_mode,
                                       sd.exclude])
 
diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py
--- a/mesonbuild/interpreter.py
+++ b/mesonbuild/interpreter.py
@@ -507,13 +507,14 @@ def get_install_dir(self):
         return self.held_object.install_dir
 
 class InstallDir(InterpreterObject):
-    def __init__(self, src_subdir, inst_subdir, install_dir, install_mode, exclude):
+    def __init__(self, src_subdir, inst_subdir, install_dir, install_mode, exclude, strip_directory):
         InterpreterObject.__init__(self)
         self.source_subdir = src_subdir
         self.installable_subdir = inst_subdir
         self.install_dir = install_dir
         self.install_mode = install_mode
         self.exclude = exclude
+        self.strip_directory = strip_directory
 
 class Man(InterpreterObject):
 
@@ -1390,7 +1391,7 @@ def get_cross_property_method(self, args, kwargs):
                     'install_data': {'install_dir', 'install_mode', 'sources'},
                     'install_headers': {'install_dir', 'subdir'},
                     'install_man': {'install_dir'},
-                    'install_subdir': {'exclude_files', 'exclude_directories', 'install_dir', 'install_mode'},
+                    'install_subdir': {'exclude_files', 'exclude_directories', 'install_dir', 'install_mode', 'strip_directory'},
                     'jar': jar_kwargs,
                     'project': {'version', 'meson_version', 'default_options', 'license', 'subproject_dir'},
                     'run_target': {'command', 'depends'},
@@ -2674,6 +2675,12 @@ def func_install_subdir(self, node, args, kwargs):
         install_dir = kwargs['install_dir']
         if not isinstance(install_dir, str):
             raise InvalidArguments('Keyword argument install_dir not a string.')
+        if 'strip_directory' in kwargs:
+            if not isinstance(kwargs['strip_directory'], bool):
+                raise InterpreterException('"strip_directory" keyword must be a boolean.')
+            strip_directory = kwargs['strip_directory']
+        else:
+            strip_directory = False
         if 'exclude_files' in kwargs:
             exclude = extract_as_list(kwargs, 'exclude_files')
             for f in exclude:
@@ -2696,7 +2703,7 @@ def func_install_subdir(self, node, args, kwargs):
             exclude_directories = set()
         exclude = (exclude_files, exclude_directories)
         install_mode = self._get_kwarg_install_mode(kwargs)
-        idir = InstallDir(self.subdir, subdir, install_dir, install_mode, exclude)
+        idir = InstallDir(self.subdir, subdir, install_dir, install_mode, exclude, strip_directory)
         self.build.install_dirs.append(idir)
         return idir
 


Test patch:
diff --git a/test cases/common/66 install subdir/installed_files.txt b/test cases/common/66 install subdir/installed_files.txt
--- a/test cases/common/66 install subdir/installed_files.txt	
+++ b/test cases/common/66 install subdir/installed_files.txt	
@@ -1,3 +1,9 @@
+usr/share/dircheck/fifth.dat
+usr/share/dircheck/seventh.dat
+usr/share/dircheck/nineth.dat
+usr/share/eighth.dat
+usr/share/fourth.dat
+usr/share/sixth.dat
 usr/share/sub1/data1.dat
 usr/share/sub1/second.dat
 usr/share/sub1/third.dat
diff --git a/test cases/common/66 install subdir/meson.build b/test cases/common/66 install subdir/meson.build
--- a/test cases/common/66 install subdir/meson.build	
+++ b/test cases/common/66 install subdir/meson.build	
@@ -11,3 +11,7 @@ subdir('subdir')
 # and read-list perms for owner and group
 install_subdir('sub1', install_dir : 'share', install_mode : ['rwxr-x--t', 'root'])
 install_subdir('sub/sub1', install_dir : 'share')
+
+# strip_directory
+install_subdir('sub_elided', install_dir : 'share', strip_directory : true)
+install_subdir('nested_elided/sub', install_dir : 'share', strip_directory : true)
diff --git a/test cases/common/66 install subdir/nested_elided/sub/dircheck/nineth.dat b/test cases/common/66 install subdir/nested_elided/sub/dircheck/nineth.dat
new file mode 100644
--- /dev/null
+++ b/test cases/common/66 install subdir/nested_elided/sub/dircheck/nineth.dat	
@@ -0,0 +1 @@
+Nested file under nested elided directory.
diff --git a/test cases/common/66 install subdir/nested_elided/sub/eighth.dat b/test cases/common/66 install subdir/nested_elided/sub/eighth.dat
new file mode 100644
--- /dev/null
+++ b/test cases/common/66 install subdir/nested_elided/sub/eighth.dat	
@@ -0,0 +1 @@
+File in nested elided directory.
diff --git a/test cases/common/66 install subdir/sub_elided/dircheck/fifth.dat b/test cases/common/66 install subdir/sub_elided/dircheck/fifth.dat
new file mode 100644
--- /dev/null
+++ b/test cases/common/66 install subdir/sub_elided/dircheck/fifth.dat	
@@ -0,0 +1 @@
+Data file in a subdir of elided directory.
diff --git a/test cases/common/66 install subdir/sub_elided/fourth.dat b/test cases/common/66 install subdir/sub_elided/fourth.dat
new file mode 100644
--- /dev/null
+++ b/test cases/common/66 install subdir/sub_elided/fourth.dat	
@@ -0,0 +1 @@
+Test that this file is installed directly into install_dir.
diff --git a/test cases/common/66 install subdir/subdir/meson.build b/test cases/common/66 install subdir/subdir/meson.build
--- a/test cases/common/66 install subdir/subdir/meson.build	
+++ b/test cases/common/66 install subdir/subdir/meson.build	
@@ -1,3 +1,5 @@
 install_subdir('sub1', install_dir : 'share',
   # This mode will be overridden by the mode set in the outer install_subdir
   install_mode : 'rwxr-x---')
+
+install_subdir('sub_elided', install_dir : 'share', strip_directory : true)
diff --git a/test cases/common/66 install subdir/subdir/sub_elided/dircheck/seventh.dat b/test cases/common/66 install subdir/subdir/sub_elided/dircheck/seventh.dat
new file mode 100644
--- /dev/null
+++ b/test cases/common/66 install subdir/subdir/sub_elided/dircheck/seventh.dat	
@@ -0,0 +1 @@
+Nested file in a subdir.
diff --git a/test cases/common/66 install subdir/subdir/sub_elided/sixth.dat b/test cases/common/66 install subdir/subdir/sub_elided/sixth.dat
new file mode 100644
--- /dev/null
+++ b/test cases/common/66 install subdir/subdir/sub_elided/sixth.dat	
@@ -0,0 +1 @@
+Elide test file in a subdir.