Repository: mesonbuild/meson

Issue:
qt5.preprocess target error when using the same file name
So, an invocation like:
`qrc_files = qt5_mod.preprocess(qresources : 'qml.qrc')`
In two separate directories reference separate qml.qrc files will fail with:
`Tried to create target "qml_qrc", but a target of that name already exists.`

Example of this are the two directories in https://cgit.freedesktop.org/gstreamer/gst-plugins-bad/tree/tests/examples/qt/



Discussion:
This is just a matter of using a better `custom_target` name in the module. The same problem plagues the `gnome` module.

The same actually also happens with two executable() calls as well with the same name.

And there's a test case (with static libraries) that explicitly tests that this fails.

As per discussion on IRC, it's not possible to support two `BuildTarget`s of the same type (`executable` or `static_library`) with the same name, and `CustomTarget` names don't affect anything in their outputs, so that is not an issue.

What is an issue is modules not using unique names for `CustomTarget`s.

Probably we should add an optional positional argument that the user can pass to get a unique name. So something like:

```
qt5.preprocess(qresources : 'qml.qrc') # target name defaults to current behaviour
# in some other dir
qt5.preprocess('unique_name', qresources : 'qml.qrc')
```

Why bother when the name doesn't have any effect besides setting the Ninja target name and the target private dir?

If two targets are given the same name, they get the same private dir and can clobber each other. In addition:
- consistency with other target types
- non-ninja backends need a unique, user visible name (e.g. each has to be a VS "project" inside the solution)
- current implementation stores targets in dicts by name, changing takes effort and then one target type would be stored in a different data structure which is confusing

No, I mean why bother with a kwarg when the module can just handle it transparently. :)

If we can come up with a good way to generate human-understandable unique names, then let's do that sure.


Resolving patch:
diff --git a/mesonbuild/dependencies.py b/mesonbuild/dependencies.py
--- a/mesonbuild/dependencies.py
+++ b/mesonbuild/dependencies.py
@@ -92,7 +92,7 @@ def get_version(self):
         return self.version
 
 class PkgConfigDependency(Dependency):
-    pkgconfig_found = None
+    pkgbin = None
 
     def __init__(self, name, environment, kwargs):
         Dependency.__init__(self, 'pkgconfig')
@@ -109,36 +109,40 @@ def __init__(self, name, environment, kwargs):
         else:
             want_cross = environment.is_cross_build()
         self.name = name
-        if PkgConfigDependency.pkgconfig_found is None:
-            self.check_pkgconfig()
+
+        # When finding dependencies for cross-compiling, we don't care about
+        # the 'native' pkg-config
+        if want_cross:
+            if 'pkgconfig' not in env.cross_info.config['binaries']:
+                if self.required:
+                    raise DependencyException('Pkg-config binary missing from cross file')
+            else:
+                pkgbin = environment.cross_info.config['binaries']['pkgconfig']
+        # Only search for the native pkg-config the first time and
+        # store the result in the class definition
+        elif PkgConfigDependency.pkgbin is None:
+            PkgConfigDependency.pkgbin = self.check_pkgconfig()
 
         self.is_found = False
-        if not PkgConfigDependency.pkgconfig_found:
+        if not self.pkgbin:
             if self.required:
                 raise DependencyException('Pkg-config not found.')
             return
-        if environment.is_cross_build() and want_cross:
-            if "pkgconfig" not in environment.cross_info.config["binaries"]:
-                raise DependencyException('Pkg-config binary missing from cross file.')
-            pkgbin = environment.cross_info.config["binaries"]['pkgconfig']
+        if want_cross:
             self.type_string = 'Cross'
         else:
-            evar = 'PKG_CONFIG'
-            if evar in os.environ:
-                pkgbin = os.environ[evar].strip()
-            else:
-                pkgbin = 'pkg-config'
             self.type_string = 'Native'
 
-        mlog.debug('Determining dependency %s with pkg-config executable %s.' % (name, pkgbin))
-        self.pkgbin = pkgbin
+        mlog.debug('Determining dependency {!r} with pkg-config executable '
+                   '{!r}'.format(name, self.pkgbin))
         ret, self.modversion = self._call_pkgbin(['--modversion', name])
         if ret != 0:
             if self.required:
-                raise DependencyException('%s dependency %s not found.' % (self.type_string, name))
+                raise DependencyException('{} dependency {!r} not found'
+                                          ''.format(self.type_string, name))
             self.modversion = 'none'
             return
-        found_msg = ['%s dependency' % self.type_string, mlog.bold(name), 'found:']
+        found_msg = [self.type_string + ' dependency', mlog.bold(name), 'found:']
         self.version_reqs = kwargs.get('version', None)
         if self.version_reqs is None:
             self.is_found = True
@@ -236,24 +240,30 @@ def get_link_args(self):
         return self.libs
 
     def check_pkgconfig(self):
+        evar = 'PKG_CONFIG'
+        if evar in os.environ:
+            pkgbin = os.environ[evar].strip()
+        else:
+            pkgbin = 'pkg-config'
         try:
-            evar = 'PKG_CONFIG'
-            if evar in os.environ:
-                pkgbin = os.environ[evar].strip()
-            else:
-                pkgbin = 'pkg-config'
             p, out = Popen_safe([pkgbin, '--version'])[0:2]
-            if p.returncode == 0:
-                if not self.silent:
-                    mlog.log('Found pkg-config:', mlog.bold(shutil.which(pkgbin)),
-                             '(%s)' % out.strip())
-                PkgConfigDependency.pkgconfig_found = True
-                return
+            if p.returncode != 0:
+                # Set to False instead of None to signify that we've already
+                # searched for it and not found it
+                pkgbin = False
         except (FileNotFoundError, PermissionError):
-            pass
-        PkgConfigDependency.pkgconfig_found = False
+            pkgbin = False
+        if pkgbin and not os.path.isabs(pkgbin) and shutil.which(pkgbin):
+            # Sometimes shutil.which fails where Popen succeeds, so
+            # only find the abs path if it can be found by shutil.which
+            pkgbin = shutil.which(pkgbin)
         if not self.silent:
-            mlog.log('Found Pkg-config:', mlog.red('NO'))
+            if pkgbin:
+                mlog.log('Found pkg-config:', mlog.bold(pkgbin),
+                         '(%s)' % out.strip())
+            else:
+                mlog.log('Found Pkg-config:', mlog.red('NO'))
+        return pkgbin
 
     def found(self):
         return self.is_found
@@ -373,6 +383,8 @@ def found(self):
         return self.is_found
 
 class ExternalProgram():
+    windows_exts = ('exe', 'com', 'bat')
+
     def __init__(self, name, fullpath=None, silent=False, search_dir=None):
         self.name = name
         if fullpath is not None:
@@ -410,11 +422,10 @@ def _shebang_to_cmd(script):
             pass
         return False
 
-    @staticmethod
-    def _is_executable(path):
+    def _is_executable(self, path):
         suffix = os.path.splitext(path)[-1].lower()[1:]
         if mesonlib.is_windows():
-            if suffix == 'exe' or suffix == 'com' or suffix == 'bat':
+            if suffix in self.windows_exts:
                 return True
         elif os.access(path, os.X_OK):
             return True
@@ -424,10 +435,15 @@ def _search_dir(self, name, search_dir):
         if search_dir is None:
             return False
         trial = os.path.join(search_dir, name)
-        if not os.path.exists(trial):
+        if os.path.exists(trial):
+            if self._is_executable(trial):
+                return [trial]
+        else:
+            for ext in self.windows_exts:
+                trial_ext = '{}.{}'.format(trial, ext)
+                if os.path.exists(trial_ext):
+                    return [trial_ext]
             return False
-        if self._is_executable(trial):
-            return [trial]
         # Now getting desperate. Maybe it is a script file that is a) not chmodded
         # executable or b) we are on windows so they can't be directly executed.
         return self._shebang_to_cmd(trial)
@@ -441,6 +457,11 @@ def _search(self, name, search_dir):
         if fullpath or not mesonlib.is_windows():
             # On UNIX-like platforms, the standard PATH search is enough
             return [fullpath]
+        # On Windows, if name is an absolute path, we need the extension too
+        for ext in self.windows_exts:
+            fullpath = '{}.{}'.format(name, ext)
+            if os.path.exists(fullpath):
+                return [fullpath]
         # On Windows, interpreted scripts must have an extension otherwise they
         # cannot be found by a standard PATH search. So we do a custom search
         # where we manually search for a script with a shebang in PATH.
@@ -1018,8 +1039,9 @@ def get_exe_args(self):
         # penalty when using self-built Qt or on platforms
         # where -fPIC is not required. If this is an issue
         # for you, patches are welcome.
-        # Fix this to be more portable, especially to MSVC.
-        return ['-fPIC']
+        if mesonlib.is_linux():
+            return ['-fPIC']
+        return []
 
 class Qt5Dependency(QtBaseDependency):
     def __init__(self, env, kwargs):
diff --git a/mesonbuild/modules/qt4.py b/mesonbuild/modules/qt4.py
--- a/mesonbuild/modules/qt4.py
+++ b/mesonbuild/modules/qt4.py
@@ -107,10 +107,10 @@ def preprocess(self, state, args, kwargs):
         moc_sources = kwargs.pop('moc_sources', [])
         if not isinstance(moc_sources, list):
             moc_sources = [moc_sources]
-        srctmp = kwargs.pop('sources', [])
-        if not isinstance(srctmp, list):
-            srctmp = [srctmp]
-        sources = args[1:] + srctmp
+        sources = kwargs.pop('sources', [])
+        if not isinstance(sources, list):
+            sources = [sources]
+        sources += args[1:]
         self._detect_tools(state.environment)
         err_msg = "{0} sources specified and couldn't find {1}, " \
                   "please check your qt4 installation"
@@ -122,8 +122,11 @@ def preprocess(self, state, args, kwargs):
             qrc_deps = []
             for i in rcc_files:
                 qrc_deps += self.parse_qrc(state, i)
-            basename = os.path.split(rcc_files[0])[1]
-            name = 'qt4-' + basename.replace('.', '_')
+            if len(args) > 0:
+                name = args[0]
+            else:
+                basename = os.path.split(rcc_files[0])[1]
+                name = 'qt4-' + basename.replace('.', '_')
             rcc_kwargs = {'input' : rcc_files,
                     'output' : name + '.cpp',
                     'command' : [self.rcc, '-o', '@OUTPUT@', '@INPUT@'],
diff --git a/mesonbuild/modules/qt5.py b/mesonbuild/modules/qt5.py
--- a/mesonbuild/modules/qt5.py
+++ b/mesonbuild/modules/qt5.py
@@ -113,10 +113,10 @@ def preprocess(self, state, args, kwargs):
         moc_sources = kwargs.pop('moc_sources', [])
         if not isinstance(moc_sources, list):
             moc_sources = [moc_sources]
-        srctmp = kwargs.pop('sources', [])
-        if not isinstance(srctmp, list):
-            srctmp = [srctmp]
-        sources = args[1:] + srctmp
+        sources = kwargs.pop('sources', [])
+        if not isinstance(sources, list):
+            sources = [sources]
+        sources += args[1:]
         self._detect_tools(state.environment)
         err_msg = "{0} sources specified and couldn't find {1}, " \
                   "please check your qt5 installation"
@@ -128,13 +128,16 @@ def preprocess(self, state, args, kwargs):
             qrc_deps = []
             for i in rcc_files:
                 qrc_deps += self.parse_qrc(state, i)
-            basename = os.path.split(rcc_files[0])[1]
+            if len(args) > 0:
+                name = args[0]
+            else:
+                basename = os.path.split(rcc_files[0])[1]
+                name = 'qt5-' + basename.replace('.', '_')
             rcc_kwargs = {'input' : rcc_files,
-                    'output' : basename + '.cpp',
+                    'output' : name + '.cpp',
                     'command' : [self.rcc, '-o', '@OUTPUT@', '@INPUT@'],
                     'depend_files' : qrc_deps,
                     }
-            name = 'qt5-' + basename.replace('.', '_')
             res_target = build.CustomTarget(name, state.subdir, rcc_kwargs)
             sources.append(res_target)
         if len(ui_files) > 0:


Test patch:
diff --git a/test cases/frameworks/4 qt/meson.build b/test cases/frameworks/4 qt/meson.build
--- a/test cases/frameworks/4 qt/meson.build	
+++ b/test cases/frameworks/4 qt/meson.build	
@@ -30,6 +30,9 @@ foreach qt : ['qt4', 'qt5']
       qresources : ['stuff.qrc', 'stuff2.qrc'], # Resource file for rcc compiler.
     )
 
+    # Test that setting a unique name with a positional argument works
+    qtmodule.preprocess(qt + 'teststuff', qresources : ['stuff.qrc'])
+
     qexe = executable(qt + 'app',
       sources : ['main.cpp', 'mainWindow.cpp', # Sources that don't need preprocessing.
       prep],