Repository: mesonbuild/meson

Issue:
localstatedir defaults to /usr/var
This is discussed somewhat in #68, which correctly identifies this is the default in the [GNU Coding Standards](https://www.gnu.org/prep/standards/html_node/Directory-Variables.html), but that this directory doesn't exist...

FHS-conforming systems appear to have a `/usr/share/config.site` (as documented [here](https://www.gnu.org/software/autoconf/manual/autoconf-2.69/html_node/Site-Defaults.html)), to override the autoconf defaults to FHS-conforming values, if the prefix is /usr.  e.g if prefix=/usr then default sysconfdir=/etc, sharedstatedir=/var and localstatedir=/var.

Then again, the current default might be right on Hurd. :smile:

(If the default is an absolute path, there's probably also a documentation issue here that the correct way to use this option in a meson.build is `join_paths(get_option('prefix'), get_option('localstatedir')))` so the correct path is used if localstatedir is an absolute path or relative to prefix.)


Discussion:
1) The option is relative to prefix because not everyone installs into `/usr` by default. For instance, people install inside their home-directory for testing. Perhaps we could set the value to `/var` when prefix is `/usr`, but that seems like a surprising thing to do.

2) When specifying `install_dir:`, you should always use `get_option('localstatedir')` since the directory is relative to `prefix` if the value is a relative directory.
> The option is relative to prefix because not everyone installs into /usr by default. For instance, people install inside their home-directory for testing. Perhaps we could set the value to /var when prefix is /usr, but that seems like a surprising thing to do.

This is what I was expecting since it's what autotools does, for me.  Maybe I'm just lucky to have a config.site which does that.  It's also more useful than using /usr/var, which then causes my built program to fail because that directory doesn't exist.

> When specifying install_dir:, you should always use get_option('localstatedir') since the directory is relative to prefix if the value is a relative directory.

Installing files into localstatedir is possibly not done very often, I was more thinking of constructing a path in localstatedir to bake into the built program via a define etc.

`/usr/var` is just plain wrong. Putting variable state there goes against the purpose of `/usr`, as it is used on modern linux distros. Quoting https://www.freedesktop.org/software/systemd/man/file-hierarchy.html#/usr:

> Usually read-only, [...]. Possibly shared between multiple hosts. This directory should not be modified by the administrator, [...]

FHS is outdated here.

> Perhaps we could set the value to /var when prefix is /usr

This seems like the right thing to do. prefix=/usr → localstatedir=/var, prefix=/usr/local, localstatedir=/var/local.
Prefix is the top-level; if we want to add something like `config.site` so distros can change the defaults when prefix is not set, then that might work. But having the default be outside prefix and/or ignoring `--prefix` because of it is IMO broken. If I configure things with `--prefix=$HOME/.local`, that's where things should be installed.
Sure, for anything which is under /home, putting everything under `--prefix` is the right thing. But not if the prefix starts with /usr.
I think we can special-case only two directories: `/usr` and `/usr/local` since those are very commonly-used and we know what the default values should be in those cases. Anything outside of that is an edge-case that the user must handle themselves.
Should we do it for `/usr/local`? That means it could overwrite system versions of `/etc` and `/var` without being explicit.
Nothing on the system will ever write to `/var/local`. That exists specifically for programs installed into `/usr/local`.
TingPing means it would be bad to ever write to `/var` if prefix is `/usr/local`. When prefix is `/usr/local` the user assumes nothing is going to overwrite system packages. It would be really unexpected for meson to install files outside the prefix without warning, unless the prefix is `/usr`. I think you should only special-case `/usr`.
If it was special cased to `/var/local` as mentioned that would be acceptable but that just adds more exceptions to understand.

Resolving patch:
diff --git a/mesonbuild/coredata.py b/mesonbuild/coredata.py
--- a/mesonbuild/coredata.py
+++ b/mesonbuild/coredata.py
@@ -1,3 +1,4 @@
+
 # Copyright 2012-2017 The Meson development team
 
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -245,7 +246,7 @@ def init_builtins(self, options):
                 value = self.sanitize_dir_option_value(options.prefix, key, value)
                 setattr(options, key, value)
             else:
-                value = get_builtin_option_default(key)
+                value = get_builtin_option_default(key, prefix=options.prefix)
             args = [key] + builtin_options[key][1:-1] + [value]
             self.builtins[key] = builtin_options[key][0](*args)
 
@@ -321,11 +322,19 @@ def get_builtin_option_description(optname):
     else:
         raise RuntimeError('Tried to get the description for an unknown builtin option \'%s\'.' % optname)
 
-def get_builtin_option_default(optname):
+def get_builtin_option_default(optname, prefix='', noneIfSuppress=False):
     if is_builtin_option(optname):
         o = builtin_options[optname]
         if o[0] == UserComboOption:
             return o[3]
+        if optname in builtin_dir_noprefix_options:
+            if noneIfSuppress:
+                # Return None if argparse defaulting should be suppressed for
+                # this option (so we can determine the default later based on
+                # prefix)
+                return None
+            elif prefix in builtin_dir_noprefix_options[optname]:
+                return builtin_dir_noprefix_options[optname][prefix]
         return o[2]
     else:
         raise RuntimeError('Tried to get the default value for an unknown builtin option \'%s\'.' % optname)
@@ -344,15 +353,6 @@ def get_builtin_option_default(optname):
     'mandir':     [UserStringOption, 'Manual page directory.', 'share/man'],
     'infodir':    [UserStringOption, 'Info page directory.', 'share/info'],
     'localedir':  [UserStringOption, 'Locale data directory.', 'share/locale'],
-    # sysconfdir, localstatedir and sharedstatedir are a bit special. These defaults to ${prefix}/etc,
-    # ${prefix}/var and ${prefix}/com but nobody uses that. Instead they always set it
-    # manually to /etc, /var and /var/lib. This default values is thus pointless and not really used
-    # but we set it to this for consistency with other systems.
-    #
-    # Projects installing to sysconfdir, localstatedir or sharedstatedir probably want
-    # to set the following in project():
-    #
-    # default_options : ['sysconfdir=/etc', 'localstatedir=/var', 'sharedstatedir=/var/lib']
     'sysconfdir':      [UserStringOption, 'Sysconf data directory.', 'etc'],
     'localstatedir':   [UserStringOption, 'Localstate data directory.', 'var'],
     'sharedstatedir':  [UserStringOption, 'Architecture-independent data directory.', 'com'],
@@ -365,8 +365,13 @@ def get_builtin_option_default(optname):
     'errorlogs':       [UserBooleanOption, "Whether to print the logs from failing tests.", True],
 }
 
-# Installation directories that can reside in a path outside of the prefix
-builtin_dir_noprefix_options = {'sysconfdir', 'localstatedir', 'sharedstatedir'}
+# Special prefix-dependent defaults for installation directories that reside in
+# a path outside of the prefix in FHS and common usage.
+builtin_dir_noprefix_options = {
+    'sysconfdir':     {'/usr': '/etc'},
+    'localstatedir':  {'/usr': '/var',     '/usr/local': '/var/local'},
+    'sharedstatedir': {'/usr': '/var/lib', '/usr/local': '/var/local/lib'},
+}
 
 forbidden_target_names = {'clean': None,
                           'clean-ctlist': None,
diff --git a/mesonbuild/mesonmain.py b/mesonbuild/mesonmain.py
--- a/mesonbuild/mesonmain.py
+++ b/mesonbuild/mesonmain.py
@@ -37,7 +37,12 @@ def add_builtin_argument(name, **kwargs):
         h = h.rstrip('.') + ' (default: %s).' % coredata.get_builtin_option_default(k)
     if c and not b:
         kwargs['choices'] = c
-    parser.add_argument('--' + name, default=coredata.get_builtin_option_default(k), help=h, **kwargs)
+    default = coredata.get_builtin_option_default(k, noneIfSuppress=True)
+    if default is not None:
+        kwargs['default'] = default
+    else:
+        kwargs['default'] = argparse.SUPPRESS
+    parser.add_argument('--' + name, help=h, **kwargs)
 
 add_builtin_argument('prefix')
 add_builtin_argument('libdir')
diff --git a/run_unittests.py b/run_unittests.py
--- a/run_unittests.py
+++ b/run_unittests.py
@@ -1,3 +1,4 @@
+
 #!/usr/bin/env python3
 # Copyright 2016-2017 The Meson development team
 
@@ -490,7 +491,10 @@ def _print_meson_log(self):
             print(f.read())
 
     def tearDown(self):
-        windows_proof_rmtree(self.builddir)
+        try:
+            windows_proof_rmtree(self.builddir)
+        except FileNotFoundError:
+            pass
         os.environ = self.orig_env
         super().tearDown()
 
@@ -739,6 +743,49 @@ def test_libdir_must_be_inside_prefix(self):
         self.init(testdir)
         self.assertRaises(subprocess.CalledProcessError, self.setconf, '-Dlibdir=/opt', False)
 
+    def test_prefix_dependent_defaults(self):
+        '''
+        Tests that configured directory paths are set to prefix dependent
+        defaults.
+        '''
+        testdir = os.path.join(self.common_test_dir, '1 trivial')
+        expected = {
+            '/opt': {'prefix': '/opt',
+                     'bindir': 'bin', 'datadir': 'share', 'includedir': 'include',
+                     'infodir': 'share/info',
+                     'libexecdir': 'libexec', 'localedir': 'share/locale',
+                     'localstatedir': 'var', 'mandir': 'share/man',
+                     'sbindir': 'sbin', 'sharedstatedir': 'com',
+                     'sysconfdir': 'etc'},
+            '/usr': {'prefix': '/usr',
+                     'bindir': 'bin', 'datadir': 'share', 'includedir': 'include',
+                     'infodir': 'share/info',
+                     'libexecdir': 'libexec', 'localedir': 'share/locale',
+                     'localstatedir': '/var', 'mandir': 'share/man',
+                     'sbindir': 'sbin', 'sharedstatedir': '/var/lib',
+                     'sysconfdir': '/etc'},
+            '/usr/local': {'prefix': '/usr/local',
+                           'bindir': 'bin', 'datadir': 'share',
+                           'includedir': 'include', 'infodir': 'share/info',
+                           'libexecdir': 'libexec',
+                           'localedir': 'share/locale',
+                           'localstatedir': '/var/local', 'mandir': 'share/man',
+                           'sbindir': 'sbin', 'sharedstatedir': '/var/local/lib',
+                           'sysconfdir': 'etc'},
+            # N.B. We don't check 'libdir' as it's platform dependent, see
+            # default_libdir():
+        }
+        for prefix in expected:
+            args = ['--prefix', prefix]
+            self.init(testdir, args, default_args=False)
+            opts = self.introspect('--buildoptions')
+            for opt in opts:
+                name = opt['name']
+                value = opt['value']
+                if name in expected[prefix]:
+                    self.assertEqual(value, expected[prefix][name])
+            self.wipe()
+
     def test_static_library_overwrite(self):
         '''
         Tests that static libraries are never appended to, always overwritten.


Test patch: