Repository: mesonbuild/meson

Issue:
Can't create a library from link_whole static libraries and without any source files
I have a project that is designed with several subprojects that are built into static libraries, then a larger library which is linked from all of them like so:

```meson
lib_wlr = library('wlroots', [],
    link_whole: [
        lib_wl_protos,
        lib_wlr_backend,
        lib_wlr_render,
        lib_wlr_types,
        lib_wlr_util,
        lib_wlr_xcursor,
    ],
    dependencies: _wlr_deps,
    include_directories: wlr_inc)
```

This fails: "Build target wlroots has no sources"

This commit includes our workaround: https://git.io/v7HNX


Discussion:
This is a good point, we need meson to realize that static libraries added with `link_whole:` are also 'sources' for a static or shared library, and perhaps also an executable.
It would also be nice if the sources parameter was optional so that I didn't have to specify an empty array in such cases.
That is already the case. You can have a library consisting of only `objects:` and no sources, which is why this bug is 'easy' to fix.
Ah, gotcha.
Is it reasonable to be able to have a static library without any sources or objects?

I'm adding files to a static library based on configure tests, and if everything checks out and no compatibility code is required, the library will be empty. My current workaround is to add an empty dummy source file to the library, which feels a bit silly.

You can't actually invoke ar without any objects, so no, that doesn't make sense.
@SirCmpwn you didn't actually try that, did you? 'ar rcs foo.a' happily creates an archive without objects.
You're right, I didn't try it, and it does work.

Resolving patch:
diff --git a/mesonbuild/backend/backends.py b/mesonbuild/backend/backends.py
--- a/mesonbuild/backend/backends.py
+++ b/mesonbuild/backend/backends.py
@@ -155,6 +155,12 @@ def get_target_dir(self, target):
             dirname = 'meson-out'
         return dirname
 
+    def get_target_dir_relative_to(self, t, o):
+        '''Get a target dir relative to another target's directory'''
+        target_dir = os.path.join(self.environment.get_build_dir(), self.get_target_dir(t))
+        othert_dir = os.path.join(self.environment.get_build_dir(), self.get_target_dir(o))
+        return os.path.relpath(target_dir, othert_dir)
+
     def get_target_source_dir(self, target):
         dirname = os.path.join(self.build_to_src, self.get_target_dir(target))
         return dirname
diff --git a/mesonbuild/backend/vs2010backend.py b/mesonbuild/backend/vs2010backend.py
--- a/mesonbuild/backend/vs2010backend.py
+++ b/mesonbuild/backend/vs2010backend.py
@@ -333,6 +333,11 @@ def target_to_build_root(self, target):
     def quote_arguments(self, arr):
         return ['"%s"' % i for i in arr]
 
+    def add_project_reference(self, root, include, projid):
+        ig = ET.SubElement(root, 'ItemGroup')
+        pref = ET.SubElement(ig, 'ProjectReference', Include=include)
+        ET.SubElement(pref, 'Project').text = '{%s}' % projid
+
     def create_basic_crap(self, target):
         project_name = target.name
         root = ET.Element('Project', {'DefaultTargets': "Build",
@@ -527,6 +532,8 @@ def split_link_args(args):
                 if lpath in lpaths:
                     lpaths.remove(lpath)
                 lpaths.append(lpath)
+            elif arg.startswith(('/', '-')):
+                other.append(arg)
             # It's ok if we miss libraries with non-standard extensions here.
             # They will go into the general link arguments.
             elif arg.endswith('.lib') or arg.endswith('.a'):
@@ -897,20 +904,26 @@ def gen_vcxproj(self, target, ofname, guid):
         # *_winlibs that we want to link to are static mingw64 libraries.
         extra_link_args += compiler.get_option_link_args(self.environment.coredata.compiler_options)
         (additional_libpaths, additional_links, extra_link_args) = self.split_link_args(extra_link_args.to_native())
-        if len(extra_link_args) > 0:
-            extra_link_args.append('%(AdditionalOptions)')
-            ET.SubElement(link, "AdditionalOptions").text = ' '.join(extra_link_args)
-        if len(additional_libpaths) > 0:
-            additional_libpaths.insert(0, '%(AdditionalLibraryDirectories)')
-            ET.SubElement(link, 'AdditionalLibraryDirectories').text = ';'.join(additional_libpaths)
 
         # Add more libraries to be linked if needed
         for t in target.get_dependencies():
             lobj = self.build.targets[t.get_id()]
             linkname = os.path.join(down, self.get_target_filename_for_linking(lobj))
             if t in target.link_whole_targets:
-                linkname = compiler.get_link_whole_for(linkname)[0]
-            additional_links.append(linkname)
+                # /WHOLEARCHIVE:foo must go into AdditionalOptions
+                extra_link_args += compiler.get_link_whole_for(linkname)
+                # To force Visual Studio to build this project even though it
+                # has no sources, we include a reference to the vcxproj file
+                # that builds this target. Technically we should add this only
+                # if the current target has no sources, but it doesn't hurt to
+                # have 'extra' references.
+                trelpath = self.get_target_dir_relative_to(t, target)
+                tvcxproj = os.path.join(trelpath, t.get_id() + '.vcxproj')
+                tid = self.environment.coredata.target_guids[t.get_id()]
+                self.add_project_reference(root, tvcxproj, tid)
+            else:
+                # Other libraries go into AdditionalDependencies
+                additional_links.append(linkname)
         for lib in self.get_custom_target_provided_libraries(target):
             additional_links.append(self.relpath(lib, self.get_target_dir(target)))
         additional_objects = []
@@ -919,6 +932,13 @@ def gen_vcxproj(self, target, ofname, guid):
             additional_objects.append(o)
         for o in custom_objs:
             additional_objects.append(o)
+
+        if len(extra_link_args) > 0:
+            extra_link_args.append('%(AdditionalOptions)')
+            ET.SubElement(link, "AdditionalOptions").text = ' '.join(extra_link_args)
+        if len(additional_libpaths) > 0:
+            additional_libpaths.insert(0, '%(AdditionalLibraryDirectories)')
+            ET.SubElement(link, 'AdditionalLibraryDirectories').text = ';'.join(additional_libpaths)
         if len(additional_links) > 0:
             additional_links.append('%(AdditionalDependencies)')
             ET.SubElement(link, 'AdditionalDependencies').text = ';'.join(additional_links)
@@ -1009,9 +1029,8 @@ def gen_vcxproj(self, target, ofname, guid):
 
         ET.SubElement(root, 'Import', Project='$(VCTargetsPath)\Microsoft.Cpp.targets')
         # Reference the regen target.
-        ig = ET.SubElement(root, 'ItemGroup')
-        pref = ET.SubElement(ig, 'ProjectReference', Include=os.path.join(self.environment.get_build_dir(), 'REGEN.vcxproj'))
-        ET.SubElement(pref, 'Project').text = self.environment.coredata.regen_guid
+        regen_vcxproj = os.path.join(self.environment.get_build_dir(), 'REGEN.vcxproj')
+        self.add_project_reference(root, regen_vcxproj, self.environment.coredata.regen_guid)
         self._prettyprint_vcxproj_xml(ET.ElementTree(root), ofname)
 
     def gen_regenproj(self, project_name, ofname):
diff --git a/mesonbuild/build.py b/mesonbuild/build.py
--- a/mesonbuild/build.py
+++ b/mesonbuild/build.py
@@ -357,7 +357,7 @@ def __init__(self, name, subdir, subproject, is_cross, sources, objects, environ
         self.process_compilers()
         self.process_kwargs(kwargs, environment)
         self.check_unknown_kwargs(kwargs)
-        if not self.sources and not self.generated and not self.objects:
+        if not any([self.sources, self.generated, self.objects, self.link_whole]):
             raise InvalidArguments('Build target %s has no sources.' % name)
         self.process_compilers_late()
         self.validate_sources()


Test patch:
diff --git a/test cases/common/145 whole archive/allofme/meson.build b/test cases/common/145 whole archive/allofme/meson.build
new file mode 100644
--- /dev/null
+++ b/test cases/common/145 whole archive/allofme/meson.build	
@@ -0,0 +1 @@
+stlib = static_library('allofme', '../libfile.c')
diff --git a/test cases/common/145 whole archive/exe/meson.build b/test cases/common/145 whole archive/exe/meson.build
new file mode 100644
--- /dev/null
+++ b/test cases/common/145 whole archive/exe/meson.build	
@@ -0,0 +1,2 @@
+exe = executable('prog', '../prog.c',
+  link_with : dylib)
diff --git a/test cases/common/145 whole archive/exe2/meson.build b/test cases/common/145 whole archive/exe2/meson.build
new file mode 100644
--- /dev/null
+++ b/test cases/common/145 whole archive/exe2/meson.build	
@@ -0,0 +1 @@
+exe2 = executable('prog2', '../prog.c', link_with : dylib2)
diff --git a/test cases/common/145 whole archive/meson.build b/test cases/common/145 whole archive/meson.build
--- a/test cases/common/145 whole archive/meson.build	
+++ b/test cases/common/145 whole archive/meson.build	
@@ -1,5 +1,7 @@
 project('whole archive', 'c')
 
+add_project_arguments('-I' + meson.source_root(), language : 'c')
+
 cc = meson.get_compiler('c')
 
 if cc.get_id() == 'msvc'
@@ -8,15 +10,15 @@ if cc.get_id() == 'msvc'
   endif
 endif
 
-stlib = static_library('allofme', 'libfile.c')
-
-# Nothing in dylib.c uses func1, so the linker would throw it
-# away and thus linking the exe would fail.
-dylib = shared_library('shlib', 'dylib.c',
-  link_whole : stlib)
-
-exe = executable('prog', 'prog.c',
-  link_with : dylib)
+subdir('allofme')
+subdir('shlib')
+subdir('exe')
 
 test('prog', exe)
 
+# link_whole only
+subdir('stlib')
+subdir('wholeshlib')
+subdir('exe2')
+
+test('prog2', exe2)
diff --git a/test cases/common/145 whole archive/shlib/meson.build b/test cases/common/145 whole archive/shlib/meson.build
new file mode 100644
--- /dev/null
+++ b/test cases/common/145 whole archive/shlib/meson.build	
@@ -0,0 +1,4 @@
+# Nothing in dylib.c uses func1, so the linker would throw it
+# away and thus linking the exe would fail.
+dylib = shared_library('shlib', '../dylib.c',
+  link_whole : stlib)
diff --git a/test cases/common/145 whole archive/stlib/meson.build b/test cases/common/145 whole archive/stlib/meson.build
new file mode 100644
--- /dev/null
+++ b/test cases/common/145 whole archive/stlib/meson.build	
@@ -0,0 +1 @@
+static = static_library('static', '../dylib.c')
diff --git a/test cases/common/145 whole archive/wholeshlib/meson.build b/test cases/common/145 whole archive/wholeshlib/meson.build
new file mode 100644
--- /dev/null
+++ b/test cases/common/145 whole archive/wholeshlib/meson.build	
@@ -0,0 +1 @@
+dylib2 = shared_library('link_whole', link_whole : [stlib, static])