Repository: mesonbuild/meson

Issue:
`install_mode` in `custom_target` does not process integer values for uid/gid
**Describe the bug**
The `install_mode` kwarg for a `custom_target` cannot process integer values for `owner` and `group`.
For instance, adding `install_mode: [false, 10000, 10000]` will not change the `uid` and `gid` to `10000` on installation.

Replacing these integers with strings does not work either, as it then expects these to be actual user or group names.

**To Reproduce**
Sample `meson.build`:

```meson
project('testproject')

custom_target(
  'target',
  build_by_default: true,
  output: 'test_output',
  command: [
    'touch',
    '@OUTPUT@',
  ],
  install: true,
  install_mode: [false, 10000, 10000],
  install_dir: meson.current_source_dir(),
)
```

Then, run:
```bash
meson setup build
sudo meson install -C build
```

On Linux, this will create a file in the `build` directory, before installing that file in the current directory. At the same time, it should change the `uid` and `gid` values to `10000`, but it does not.

Looking at the code, I believe this comes from `mesonbuild/interpreter/typechecking.py`. The `_install_mode_convertor` function replaces all non string values with `None`:
https://github.com/mesonbuild/meson/blob/84c412a921497432cc6884befddd0b29c3a389cd/mesonbuild/interpreter/type_checking.py#L97-L106

**Expected behavior**
As written in the [docs](https://mesonbuild.com/Reference-manual_functions.html#custom_target), the `install_mode` kwarg should process correctly integer values for owner and group for a `custom_target`.

**system parameters**
Ubuntu 22.04.1
Python 3.10.6
meson 1.0.0
ninja 1.10.1



Discussion:
I haven't checked whether this is an issue or not, but your test case is fundamentally wrong as it doesn't try to install using sudo or some other method of acquiring root, which means that setting uid/gid is disabled no matter what. The only other option would be to error out...
True, I will update the issue.

It does not change the problem, though.

Resolving patch:
diff --git a/mesonbuild/coredata.py b/mesonbuild/coredata.py
--- a/mesonbuild/coredata.py
+++ b/mesonbuild/coredata.py
@@ -191,7 +191,7 @@ def printable_value(self) -> str:
         return format(self.value, '04o')
 
     def validate_value(self, value: T.Any) -> T.Union[str, OctalInt]:
-        if value is None or value == 'preserve':
+        if value == 'preserve':
             return 'preserve'
         return OctalInt(super().validate_value(value))
 
diff --git a/mesonbuild/interpreter/type_checking.py b/mesonbuild/interpreter/type_checking.py
--- a/mesonbuild/interpreter/type_checking.py
+++ b/mesonbuild/interpreter/type_checking.py
@@ -95,15 +95,19 @@ def _install_mode_validator(mode: T.List[T.Union[str, bool, int]]) -> T.Optional
 
 
 def _install_mode_convertor(mode: T.Optional[T.List[T.Union[str, bool, int]]]) -> FileMode:
-    """Convert the DSL form of the `install_mode` keyword argument to `FileMode`
+    """Convert the DSL form of the `install_mode` keyword argument to `FileMode`"""
 
-    This is not required, and if not required returns None
+    if not mode:
+        return FileMode()
 
-    TODO: It's not clear to me why this needs to be None and not just return an
-    empty FileMode.
-    """
-    # this has already been validated by the validator
-    return FileMode(*(m if isinstance(m, str) else None for m in mode))
+    # This has already been validated by the validator. False denotes "use
+    # default". mypy is totally incapable of understanding it, because
+    # generators clobber types via homogeneous return. But also we *must*
+    # convert the first element different from the rest
+    m1 = mode[0] if isinstance(mode[0], str) else None
+    rest = (m if isinstance(m, (str, int)) else None for m in mode[1:])
+
+    return FileMode(m1, *rest)
 
 
 def _lower_strlist(input: T.List[str]) -> T.List[str]:


Test patch: