Repository: mesonbuild/meson

Issue:
want install_data() variant that allows installing file under a different name
Sometimes one needs to install a file (data), but under a different name than it exists in the source tree, e.g. install `foo.bar.win32` as `foo.bar`.

We can currently hack around that with `configure_file()` and an empty configuration, but would be nice if there was an official way too.


Discussion:
As stated in the last notification, tp-m has pointed me to this thread in #860. I'm facing a similar problem where some images have to be copied with their name changed. The case is exactly like this:

The files paths are as follow:

* data/icons/hicolor_apps_16x16.png
* data/icons/hicolor_apps_24x24.png
* data/icons/hicolor_apps_48x48.png
*  ...

These files should be installed like this:

* icons/hicolor/16x16/apps/note.png
* icons/hicolor/24x24/apps/note.png
* icons/hicolor/48x48/apps/note.png
* ...

The configuration workaround has two problems here.

1. It will parse the binary file, while it's unnecessary.
2. I'm not able to rename.

It would be nice if something like a copy_file function where source file name and destination file name could be stated exists.
I did a workaround where I modified the files by using an install script, so the files were renamed properly. You can find a real example here[0], where a _very similar_ script is as follows:

`
name_pattern = re.compile('hicolor_(?:apps|actions)_(?:\d+x\d+|scalable)_(.*)')
search_pattern = '/**/hicolor_*'

actions_icon_dir = os.path.join(os.environ['MESON_INSTALL_PREFIX'], 'share', 'bijiben', 'icons', 'hicolor')
[os.rename(file, os.path.join(os.path.dirname(file), name_pattern.search(file).group(1)))
 for file in glob.glob(actions_icon_dir + search_pattern, recursive=True)]

apps_icon_dir = os.path.join(os.environ['MESON_INSTALL_PREFIX'], 'share', 'icons', 'hicolor')
[os.rename(file, os.path.join(os.path.dirname(file), name_pattern.search(file).group(1)))
`

I used some regex _black magic_ to parse the original file name, by getting the fields I need that were going to be used in the final name. It worked fine, although the files were properly renamed by an install script, meson is not able to figure out the final name on uninstall, so actually the command doesn't work properly. Despite the fact that this seems correct, it confirms that the workaround is just a hack and a rename option should exist somewhere.

I was also thinking about a _symetric_ add_uninstall_script, but it doesn't some like a good solution.

[0] https://git.gnome.org/browse/bijiben/tree/meson_post_install.py?h=wip/inigomartinez/meson
Exact same problem here, for GHex. Nautilus is doing it another way [1], by arranging icons in subdirectories in the source tree and then calling install_subdir().

Still, it would be nice to be able to rename files at install time. What about an 'install_name' named argument one would pass to install_data()? Think it would cover at least the simple use-cases, wouldn't it?

[1] https://git.gnome.org/browse/nautilus/tree/data/icons

Resolving patch:
diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py
--- a/mesonbuild/backend/ninjabackend.py
+++ b/mesonbuild/backend/ninjabackend.py
@@ -878,11 +878,10 @@ def generate_data_install(self, d):
             subdir = de.install_dir
             if not subdir:
                 subdir = os.path.join(self.environment.get_datadir(), self.interpreter.build.project_name)
-            for f in de.sources:
-                assert(isinstance(f, mesonlib.File))
-                plain_f = os.path.basename(f.fname)
-                dstabs = os.path.join(subdir, plain_f)
-                i = [f.absolute_path(srcdir, builddir), dstabs, de.install_mode]
+            for src_file, dst_name in zip(de.sources, de.rename):
+                assert(isinstance(src_file, mesonlib.File))
+                dst_abs = os.path.join(subdir, dst_name)
+                i = [src_file.absolute_path(srcdir, builddir), dst_abs, de.install_mode]
                 d.data.append(i)
 
     def generate_subdir_install(self, d):
diff --git a/mesonbuild/build.py b/mesonbuild/build.py
--- a/mesonbuild/build.py
+++ b/mesonbuild/build.py
@@ -1894,13 +1894,19 @@ def keys(self):
 # A bit poorly named, but this represents plain data files to copy
 # during install.
 class Data:
-    def __init__(self, sources, install_dir, install_mode=None):
+    def __init__(self, sources, install_dir, install_mode=None, rename=None):
         self.sources = sources
         self.install_dir = install_dir
         self.install_mode = install_mode
         self.sources = listify(self.sources)
         for s in self.sources:
             assert(isinstance(s, File))
+        if rename is None:
+            self.rename = [os.path.basename(f.fname) for f in self.sources]
+        else:
+            self.rename = stringlistify(rename)
+            if len(self.rename) != len(self.sources):
+                raise MesonException('Size of rename argument is different from number of sources')
 
 class RunScript(dict):
     def __init__(self, script, args):
diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py
--- a/mesonbuild/interpreter.py
+++ b/mesonbuild/interpreter.py
@@ -1500,7 +1500,7 @@ def get_cross_property_method(self, args, kwargs):
                     'find_program': {'required', 'native'},
                     'generator': {'arguments', 'output', 'depfile', 'capture', 'preserve_path_from'},
                     'include_directories': {'is_system'},
-                    'install_data': {'install_dir', 'install_mode', 'sources'},
+                    'install_data': {'install_dir', 'install_mode', 'rename', 'sources'},
                     'install_headers': {'install_dir', 'subdir'},
                     'install_man': {'install_dir'},
                     'install_subdir': {'exclude_files', 'exclude_directories', 'install_dir', 'install_mode', 'strip_directory'},
@@ -2826,7 +2826,8 @@ def func_install_data(self, node, args, kwargs):
         if not isinstance(install_dir, (str, type(None))):
             raise InvalidArguments('Keyword argument install_dir not a string.')
         install_mode = self._get_kwarg_install_mode(kwargs)
-        data = DataHolder(build.Data(sources, install_dir, install_mode))
+        rename = kwargs.get('rename', None)
+        data = DataHolder(build.Data(sources, install_dir, install_mode, rename))
         self.build.data.append(data.held_object)
         return data
 


Test patch:
diff --git a/test cases/common/12 data/installed_files.txt b/test cases/common/12 data/installed_files.txt
--- a/test cases/common/12 data/installed_files.txt	
+++ b/test cases/common/12 data/installed_files.txt	
@@ -2,6 +2,10 @@ usr/share/progname/datafile.dat
 usr/share/progname/fileobject_datafile.dat
 usr/share/progname/vanishing.dat
 usr/share/progname/vanishing2.dat
+usr/share/data install test/renamed file.txt
 usr/share/data install test/somefile.txt
+usr/share/data install test/some/nested/path.txt
+usr/share/renamed/renamed 2.txt
+usr/share/renamed/renamed 3.txt
 etc/etcfile.dat
 usr/bin/runscript.sh
diff --git a/test cases/common/12 data/meson.build b/test cases/common/12 data/meson.build
--- a/test cases/common/12 data/meson.build	
+++ b/test cases/common/12 data/meson.build	
@@ -15,3 +15,9 @@ install_data(files('somefile.txt'))
 subdir('vanishing')
 
 install_data(sources : 'vanishing/vanishing2.dat', install_dir : 'share/progname')
+
+install_data(sources : 'to_be_renamed_1.txt', rename : 'renamed file.txt')
+install_data(sources : ['vanishing/to_be_renamed_2.txt', 'to_be_renamed_3.txt'],
+             install_dir : 'share/renamed',
+             rename : ['renamed 2.txt', 'renamed 3.txt'])
+install_data(sources : 'to_be_renamed_4.txt', rename : 'some/nested/path.txt')
diff --git a/test cases/common/12 data/to_be_renamed_1.txt b/test cases/common/12 data/to_be_renamed_1.txt
new file mode 100644
diff --git a/test cases/common/12 data/to_be_renamed_3.txt b/test cases/common/12 data/to_be_renamed_3.txt
new file mode 100644
diff --git a/test cases/common/12 data/to_be_renamed_4.txt b/test cases/common/12 data/to_be_renamed_4.txt
new file mode 100644
diff --git a/test cases/common/12 data/vanishing/to_be_renamed_2.txt b/test cases/common/12 data/vanishing/to_be_renamed_2.txt
new file mode 100644
diff --git a/test cases/failing/70 install_data rename bad size/file1.txt b/test cases/failing/70 install_data rename bad size/file1.txt
new file mode 100644
diff --git a/test cases/failing/70 install_data rename bad size/file2.txt b/test cases/failing/70 install_data rename bad size/file2.txt
new file mode 100644
diff --git a/test cases/failing/70 install_data rename bad size/meson.build b/test cases/failing/70 install_data rename bad size/meson.build
new file mode 100644
--- /dev/null
+++ b/test cases/failing/70 install_data rename bad size/meson.build	
@@ -0,0 +1,3 @@
+project('data install test', 'c')
+
+install_data(['file1.txt', 'file2.txt'], rename : 'just one name')