Repository: mesonbuild/meson

Issue:
installed mode tests fail for non-root user on Solaris
```
======================================================================
FAIL: test_installed_modes (__main__.LinuxlikeTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "run_unittests.py", line 4579, in test_installed_modes
    self.assertEqual(want_mode, found_mode[1:])
AssertionError: 'rw------T' != 'rw-------'
- rw------T
?         ^
+ rw-------
?         ^
```

```
======================================================================
FAIL: test_installed_modes_extended (__main__.LinuxlikeTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "run_unittests.py", line 4650, in test_installed_modes_extended
    (fsobj, want_mode, found_mode)))
AssertionError: '-r--r--r-T' != '-r--r--r--'
- -r--r--r-T
?          ^
+ -r--r--r--
?          ^
 : Expected file include/rootdir.h to have mode -r--r--r-T but found -r--r--r-- instead.

```

This appears to be because [Solaris chmod(2)](https://docs.oracle.com/cd/E88353_01/html/E37841/chmod-2.html) has an additional restriction:

>        If the process is not a privileged process and the file is not a direc-
>        tory, mode bit 01000 (S_ISVTX, the sticky bit) is cleared.



Discussion:
I've seen similar issues with the sticky bit on at least FreeBSD (probably all of the BSDs).

Resolving patch:
diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py
--- a/mesonbuild/interpreter/interpreter.py
+++ b/mesonbuild/interpreter/interpreter.py
@@ -1894,6 +1894,7 @@ def func_custom_target(self, node: mparser.FunctionNode, args: T.Tuple[str],
                            kwargs: 'kwargs.CustomTarget') -> build.CustomTarget:
         if kwargs['depfile'] and ('@BASENAME@' in kwargs['depfile'] or '@PLAINNAME@' in kwargs['depfile']):
             FeatureNew.single_use('substitutions in custom_target depfile', '0.47.0', self.subproject, location=node)
+        install_mode = self._warn_kwarg_install_mode_sticky(kwargs['install_mode'])
 
         # Don't mutate the kwargs
 
@@ -1976,7 +1977,7 @@ def func_custom_target(self, node: mparser.FunctionNode, args: T.Tuple[str],
             feed=kwargs['feed'],
             install=kwargs['install'],
             install_dir=kwargs['install_dir'],
-            install_mode=kwargs['install_mode'],
+            install_mode=install_mode,
             install_tag=kwargs['install_tag'],
             backend=self.backend)
         self.add_target(tg.name, tg)
@@ -2129,6 +2130,7 @@ def add_test(self, node: mparser.BaseNode, args: T.List, kwargs: T.Dict[str, T.A
     def func_install_headers(self, node: mparser.BaseNode,
                              args: T.Tuple[T.List['mesonlib.FileOrString']],
                              kwargs: 'kwargs.FuncInstallHeaders') -> build.Headers:
+        install_mode = self._warn_kwarg_install_mode_sticky(kwargs['install_mode'])
         source_files = self.source_strings_to_files(args[0])
         install_subdir = kwargs['subdir']
         if install_subdir is not None:
@@ -2150,7 +2152,7 @@ def func_install_headers(self, node: mparser.BaseNode,
 
         for childdir in dirs:
             h = build.Headers(dirs[childdir], os.path.join(install_subdir, childdir), kwargs['install_dir'],
-                              kwargs['install_mode'], self.subproject)
+                              install_mode, self.subproject)
             ret_headers.append(h)
             self.build.headers.append(h)
 
@@ -2166,6 +2168,7 @@ def func_install_headers(self, node: mparser.BaseNode,
     def func_install_man(self, node: mparser.BaseNode,
                          args: T.Tuple[T.List['mesonlib.FileOrString']],
                          kwargs: 'kwargs.FuncInstallMan') -> build.Man:
+        install_mode = self._warn_kwarg_install_mode_sticky(kwargs['install_mode'])
         # We just need to narrow this, because the input is limited to files and
         # Strings as inputs, so only Files will be returned
         sources = self.source_strings_to_files(args[0])
@@ -2177,7 +2180,7 @@ def func_install_man(self, node: mparser.BaseNode,
             if not 1 <= num <= 9:
                 raise InvalidArguments('Man file must have a file extension of a number between 1 and 9')
 
-        m = build.Man(sources, kwargs['install_dir'], kwargs['install_mode'],
+        m = build.Man(sources, kwargs['install_dir'], install_mode,
                       self.subproject, kwargs['locale'])
         self.build.man.append(m)
 
@@ -2321,6 +2324,21 @@ def _get_kwarg_install_mode(self, kwargs: T.Dict[str, T.Any]) -> T.Optional[File
                                    'permissions arg to be a string or false')
         return FileMode(*install_mode)
 
+
+    # This is either ignored on basically any OS nowadays, or silently gets
+    # ignored (Solaris) or triggers an "illegal operation" error (FreeBSD).
+    # It was likely added "because it exists", but should never be used. In
+    # theory it is useful for directories, but we never apply modes to
+    # directories other than in install_emptydir.
+    def _warn_kwarg_install_mode_sticky(self, mode: FileMode) -> None:
+        if mode.perms > 0 and mode.perms & stat.S_ISVTX:
+            mlog.deprecation('install_mode with the sticky bit on a file does not do anything and will '
+                             f'be ignored since Meson 0.64.0', location=self.current_node)
+            perms = stat.filemode(mode.perms - stat.S_ISVTX)[1:]
+            return FileMode(perms, mode.owner, mode.group)
+        else:
+            return mode
+
     @typed_pos_args('install_data', varargs=(str, mesonlib.File))
     @typed_kwargs(
         'install_data',
@@ -2342,7 +2360,8 @@ def func_install_data(self, node: mparser.BaseNode,
                     '"rename" and "sources" argument lists must be the same length if "rename" is given. '
                     f'Rename has {len(rename)} elements and sources has {len(sources)}.')
 
-        return self.install_data_impl(sources, kwargs['install_dir'], kwargs['install_mode'],
+        install_mode = self._warn_kwarg_install_mode_sticky(kwargs['install_mode'])
+        return self.install_data_impl(sources, kwargs['install_dir'], install_mode,
                                       rename, kwargs['install_tag'],
                                       preserve_path=kwargs['preserve_path'])
 
@@ -2398,12 +2417,13 @@ def func_install_subdir(self, node: mparser.BaseNode, args: T.Tuple[str],
             FeatureNew.single_use('install_subdir with empty directory', '0.47.0', self.subproject, location=node)
             FeatureDeprecated.single_use('install_subdir with empty directory', '0.60.0', self.subproject,
                                          'It worked by accident and is buggy. Use install_emptydir instead.', node)
+        install_mode = self._warn_kwarg_install_mode_sticky(kwargs['install_mode'])
 
         idir = build.InstallDir(
             self.subdir,
             args[0],
             kwargs['install_dir'],
-            kwargs['install_mode'],
+            install_mode,
             exclude,
             kwargs['strip_directory'],
             self.subproject,
@@ -2469,6 +2489,8 @@ def func_configure_file(self, node: mparser.BaseNode, args: T.List[TYPE_var],
         if kwargs['capture'] and not kwargs['command']:
             raise InvalidArguments('configure_file: "capture" keyword requires "command" keyword.')
 
+        install_mode = self._warn_kwarg_install_mode_sticky(kwargs['install_mode'])
+
         fmt = kwargs['format']
         output_format = kwargs['output_format']
         depfile = kwargs['depfile']
@@ -2594,7 +2616,6 @@ def func_configure_file(self, node: mparser.BaseNode, args: T.List[TYPE_var],
             if isinstance(idir_name, P_OBJ.OptionString):
                 idir_name = idir_name.optname
             cfile = mesonlib.File.from_built_file(ofile_path, ofile_fname)
-            install_mode = kwargs['install_mode']
             install_tag = kwargs['install_tag']
             self.build.data.append(build.Data([cfile], idir, idir_name, install_mode, self.subproject,
                                               install_tag=install_tag, data_type='configure'))
diff --git a/unittests/linuxliketests.py b/unittests/linuxliketests.py
--- a/unittests/linuxliketests.py
+++ b/unittests/linuxliketests.py
@@ -615,7 +615,7 @@ def test_installed_modes(self):
 
         f = os.path.join(self.installdir, 'etc', 'etcfile.dat')
         found_mode = stat.filemode(os.stat(f).st_mode)
-        want_mode = 'rw------T'
+        want_mode = 'rw-------'
         self.assertEqual(want_mode, found_mode[1:])
 
         f = os.path.join(self.installdir, 'usr', 'bin', 'runscript.sh')
@@ -650,7 +650,7 @@ def test_installed_modes(self):
         f = os.path.join(self.installdir, 'usr', 'share', 'sub1', 'second.dat')
         statf = os.stat(f)
         found_mode = stat.filemode(statf.st_mode)
-        want_mode = 'rwxr-x--t'
+        want_mode = 'rwxr-x--x'
         self.assertEqual(want_mode, found_mode[1:])
         if os.getuid() == 0:
             # The chown failed nonfatally if we're not root
@@ -673,15 +673,15 @@ def test_installed_modes_extended(self):
                 ('bin/trivialprog', '-rwxr-sr-x'),
                 ('include', 'drwxr-x---'),
                 ('include/config.h', '-rw-rwSr--'),
-                ('include/rootdir.h', '-r--r--r-T'),
+                ('include/rootdir.h', '-r--r--r--'),
                 ('lib', 'drwxr-x---'),
                 ('lib/libstat.a', '-rw---Sr--'),
                 ('share', 'drwxr-x---'),
                 ('share/man', 'drwxr-x---'),
                 ('share/man/man1', 'drwxr-x---'),
-                ('share/man/man1/foo.1', '-r--r--r-T'),
+                ('share/man/man1/foo.1', '-r--r--r--'),
                 ('share/sub1', 'drwxr-x---'),
-                ('share/sub1/second.dat', '-rwxr-x--t'),
+                ('share/sub1/second.dat', '-rwxr-x--x'),
                 ('subdir', 'drwxr-x---'),
                 ('subdir/data.dat', '-rw-rwSr--'),
         ]:


Test patch:
diff --git a/test cases/common/190 install_mode/test.json b/test cases/common/190 install_mode/test.json
--- a/test cases/common/190 install_mode/test.json	
+++ b/test cases/common/190 install_mode/test.json	
@@ -11,5 +11,12 @@
     {"type": "file", "file": "usr/share/sub2/stub"},
     {"type": "file", "file": "usr/subdir/data.dat"}
   ],
-  "do_not_set_opts": ["libdir"]
+  "do_not_set_opts": ["libdir"],
+  "stdout": [
+    {
+      "line": ".* DEPRECATION: install_mode with the sticky bit on a file",
+      "match": "re",
+      "count": 3
+    }
+  ]
 }