Repository: mesonbuild/meson

Issue:
Objects of files() may be ambiguous
When files are specified using files(), their subdirectories aren't added as a prefix to their object file names, and all are output to the same output directory. As a result, if two different subdirectories have files with the same name, a conflict will follow.

For example, the following fails with `Multiple producers for Ninja target "a.out@exe/file.c.o". Please rename your targets.`:
```sh
#!/bin/sh

mkdir -p dir1 dir2

echo "int main() { return 0; }" > dir1/file.c
echo "sources += files('file.c')" > dir1/meson.build

touch dir2/file.c
echo "sources += files('file.c')" > dir2/meson.build

cat >meson.build <<EOF
project('proj', 'c')

sources = []
subdir('dir1')
subdir('dir2')
executable('a.out', sources: sources)
EOF

meson build
```


Discussion:
The fix for this is to use not the filename for deriving the object name, but the relative path to the current directory. The fix will have to also keep into account object extraction.
@nirbheek Semi-related to this, perhaps extract_objects() should be able to receive files() as arguments?
Yes, that makes sense too!

Resolving patch:
diff --git a/mesonbuild/backend/backends.py b/mesonbuild/backend/backends.py
--- a/mesonbuild/backend/backends.py
+++ b/mesonbuild/backend/backends.py
@@ -179,8 +179,9 @@ def get_target_generated_dir(self, target, gensrc, src):
         # target that the GeneratedList is used in
         return os.path.join(self.get_target_private_dir(target), src)
 
-    def get_unity_source_filename(self, target, suffix):
-        return target.name + '-unity.' + suffix
+    def get_unity_source_file(self, target, suffix):
+        osrc = target.name + '-unity.' + suffix
+        return mesonlib.File.from_built_file(self.get_target_private_dir(target), osrc)
 
     def generate_unity_files(self, target, unity_src):
         abs_files = []
@@ -188,18 +189,15 @@ def generate_unity_files(self, target, unity_src):
         compsrcs = classify_unity_sources(target.compilers.values(), unity_src)
 
         def init_language_file(suffix):
-            unity_src_name = self.get_unity_source_filename(target, suffix)
-            unity_src_subdir = self.get_target_private_dir_abs(target)
-            outfilename = os.path.join(unity_src_subdir,
-                                       unity_src_name)
-            outfileabs = os.path.join(self.environment.get_build_dir(),
-                                      outfilename)
+            unity_src = self.get_unity_source_file(target, suffix)
+            outfileabs = unity_src.absolute_path(self.environment.get_source_dir(),
+                                                 self.environment.get_build_dir())
             outfileabs_tmp = outfileabs + '.tmp'
             abs_files.append(outfileabs)
             outfileabs_tmp_dir = os.path.dirname(outfileabs_tmp)
             if not os.path.exists(outfileabs_tmp_dir):
                 os.makedirs(outfileabs_tmp_dir)
-            result.append(mesonlib.File(True, unity_src_subdir, unity_src_name))
+            result.append(unity_src)
             return open(outfileabs_tmp, 'w')
 
         # For each language, generate a unity source file and return the list
@@ -309,13 +307,35 @@ def determine_rpath_dirs(self, target):
         return result
 
     def object_filename_from_source(self, target, source, is_unity):
-        if isinstance(source, mesonlib.File):
-            source = source.fname
+        assert isinstance(source, mesonlib.File)
+        build_dir = self.environment.get_build_dir()
+        rel_src = source.rel_to_builddir(self.build_to_src)
         # foo.vala files compile down to foo.c and then foo.c.o, not foo.vala.o
-        if source.endswith(('.vala', '.gs')):
+        if rel_src.endswith(('.vala', '.gs')):
+            # See description in generate_vala_compile for this logic.
+            if source.is_built:
+                if os.path.isabs(rel_src):
+                    rel_src = rel_src[len(build_dir) + 1:]
+                rel_src = os.path.relpath(rel_src, self.get_target_private_dir(target))
+            else:
+                rel_src = os.path.basename(rel_src)
             if is_unity:
-                return source[:-5] + '.c.' + self.environment.get_object_suffix()
-            source = os.path.join(self.get_target_private_dir(target), source[:-5] + '.c')
+                return 'meson-generated_' + rel_src[:-5] + '.c.' + self.environment.get_object_suffix()
+            # A meson- prefixed directory is reserved; hopefully no-one creates a file name with such a weird prefix.
+            source = 'meson-generated_' + rel_src[:-5] + '.c'
+        elif source.is_built:
+            if os.path.isabs(rel_src):
+                rel_src = rel_src[len(build_dir) + 1:]
+            targetdir = self.get_target_private_dir(target)
+            # A meson- prefixed directory is reserved; hopefully no-one creates a file name with such a weird prefix.
+            source = 'meson-generated_' + os.path.relpath(rel_src, targetdir)
+        else:
+            if os.path.isabs(rel_src):
+                # Not from the source directory; hopefully this doesn't conflict with user's source files.
+                source = os.path.basename(rel_src)
+            else:
+                source = os.path.relpath(os.path.join(build_dir, rel_src),
+                                         os.path.join(self.environment.get_source_dir(), target.get_subdir()))
         return source.replace('/', '_').replace('\\', '_') + '.' + self.environment.get_object_suffix()
 
     def determine_ext_objs(self, target, extobj, proj_dir_to_build_root):
@@ -329,9 +349,8 @@ def determine_ext_objs(self, target, extobj, proj_dir_to_build_root):
                                            extobj.srclist[0])
             # There is a potential conflict here, but it is unlikely that
             # anyone both enables unity builds and has a file called foo-unity.cpp.
-            osrc = self.get_unity_source_filename(extobj.target,
-                                                  comp.get_default_suffix())
-            osrc = os.path.join(self.get_target_private_dir(extobj.target), osrc)
+            osrc = self.get_unity_source_file(extobj.target,
+                                              comp.get_default_suffix())
             objname = self.object_filename_from_source(extobj.target, osrc, True)
             objname = objname.replace('/', '_').replace('\\', '_')
             objpath = os.path.join(proj_dir_to_build_root, targetdir, objname)
diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py
--- a/mesonbuild/backend/ninjabackend.py
+++ b/mesonbuild/backend/ninjabackend.py
@@ -2102,39 +2102,21 @@ def generate_single_compile(self, target, outfile, src, is_generated=False, head
             self.target_arg_cache[key] = commands
         commands = CompilerArgs(commands.compiler, commands)
 
-        if isinstance(src, mesonlib.File) and src.is_built:
-            rel_src = os.path.join(src.subdir, src.fname)
-            if os.path.isabs(rel_src):
-                assert(rel_src.startswith(self.environment.get_build_dir()))
-                rel_src = rel_src[len(self.environment.get_build_dir()) + 1:]
-            abs_src = os.path.join(self.environment.get_build_dir(), rel_src)
-        elif isinstance(src, mesonlib.File):
+        build_dir = self.environment.get_build_dir()
+        if isinstance(src, File):
             rel_src = src.rel_to_builddir(self.build_to_src)
-            abs_src = src.absolute_path(self.environment.get_source_dir(),
-                                        self.environment.get_build_dir())
+            if os.path.isabs(rel_src):
+                # Source files may not be from the source directory if they originate in source-only libraries,
+                # so we can't assert that the absolute path is anywhere in particular.
+                if src.is_built:
+                    assert rel_src.startswith(build_dir)
+                    rel_src = rel_src[len(build_dir) + 1:]
         elif is_generated:
             raise AssertionError('BUG: broken generated source file handling for {!r}'.format(src))
         else:
-            if isinstance(src, File):
-                rel_src = src.rel_to_builddir(self.build_to_src)
-            else:
-                raise InvalidArguments('Invalid source type: {!r}'.format(src))
-            abs_src = os.path.join(self.environment.get_build_dir(), rel_src)
-        if isinstance(src, File):
-            if src.is_built:
-                src_filename = os.path.join(src.subdir, src.fname)
-                if os.path.isabs(src_filename):
-                    assert(src_filename.startswith(self.environment.get_build_dir()))
-                    src_filename = src_filename[len(self.environment.get_build_dir()) + 1:]
-            else:
-                src_filename = src.fname
-        elif os.path.isabs(src):
-            src_filename = os.path.basename(src)
-        else:
-            src_filename = src
-        obj_basename = src_filename.replace('/', '_').replace('\\', '_')
+            raise InvalidArguments('Invalid source type: {!r}'.format(src))
+        obj_basename = self.object_filename_from_source(target, src, self.is_unity(target))
         rel_obj = os.path.join(self.get_target_private_dir(target), obj_basename)
-        rel_obj += '.' + self.environment.get_object_suffix()
         dep_file = compiler.depfile_for_object(rel_obj)
 
         # Add MSVC debug file generation compile flags: /Fd /FS
@@ -2167,6 +2149,7 @@ def generate_single_compile(self, target, outfile, src, is_generated=False, head
             # outdir argument instead.
             # https://github.com/mesonbuild/meson/issues/1348
             if not is_generated:
+                abs_src = os.path.join(build_dir, rel_src)
                 extra_deps += self.get_fortran_deps(compiler, abs_src, target)
             # Dependency hack. Remove once multiple outputs in Ninja is fixed:
             # https://groups.google.com/forum/#!topic/ninja-build/j-2RfBIOd_8
diff --git a/mesonbuild/backend/vs2010backend.py b/mesonbuild/backend/vs2010backend.py
--- a/mesonbuild/backend/vs2010backend.py
+++ b/mesonbuild/backend/vs2010backend.py
@@ -80,39 +80,10 @@ def __init__(self, build):
         super().__init__(build)
         self.name = 'vs2010'
         self.project_file_version = '10.0.30319.1'
-        self.sources_conflicts = {}
         self.platform_toolset = None
         self.vs_version = '2010'
         self.windows_target_platform_version = None
 
-    def object_filename_from_source(self, target, source, is_unity=False):
-        basename = os.path.basename(source.fname)
-        filename_without_extension = '.'.join(basename.split('.')[:-1])
-        if basename in self.sources_conflicts[target.get_id()]:
-            # If there are multiple source files with the same basename, we must resolve the conflict
-            # by giving each a unique object output file.
-            filename_without_extension = '.'.join(source.fname.split('.')[:-1]).replace('/', '_').replace('\\', '_')
-        return filename_without_extension + '.' + self.environment.get_object_suffix()
-
-    def resolve_source_conflicts(self):
-        for name, target in self.build.targets.items():
-            if not isinstance(target, BuildTarget):
-                continue
-            conflicts = {}
-            for s in target.get_sources():
-                if hasattr(s, 'held_object'):
-                    s = s.held_object
-                if not isinstance(s, File):
-                    continue
-                basename = os.path.basename(s.fname)
-                conflicting_sources = conflicts.get(basename, None)
-                if conflicting_sources is None:
-                    conflicting_sources = []
-                    conflicts[basename] = conflicting_sources
-                conflicting_sources.append(s)
-            self.sources_conflicts[target.get_id()] = {name: src_conflicts for name, src_conflicts in conflicts.items()
-                                                       if len(src_conflicts) > 1}
-
     def generate_custom_generator_commands(self, target, parent_node):
         generator_output_files = []
         custom_target_include_dirs = []
@@ -164,7 +135,6 @@ def generate_custom_generator_commands(self, target, parent_node):
         return generator_output_files, custom_target_output_files, custom_target_include_dirs
 
     def generate(self, interp):
-        self.resolve_source_conflicts()
         self.interpreter = interp
         target_machine = self.interpreter.builtin['target_machine'].cpu_family_method(None, None)
         if target_machine.endswith('64'):
@@ -1003,9 +973,7 @@ def gen_vcxproj(self, target, ofname, guid):
                 self.add_additional_options(lang, inc_cl, file_args)
                 self.add_preprocessor_defines(lang, inc_cl, file_defines)
                 self.add_include_dirs(lang, inc_cl, file_inc_dirs)
-                basename = os.path.basename(s.fname)
-                if basename in self.sources_conflicts[target.get_id()]:
-                    ET.SubElement(inc_cl, 'ObjectFileName').text = "$(IntDir)" + self.object_filename_from_source(target, s)
+                ET.SubElement(inc_cl, 'ObjectFileName').text = "$(IntDir)" + self.object_filename_from_source(target, s, False)
             for s in gen_src:
                 inc_cl = ET.SubElement(inc_src, 'CLCompile', Include=s)
                 lang = Vs2010Backend.lang_from_source_file(s)


Test patch:
diff --git a/test cases/common/160 duplicate source names/dir1/file.c b/test cases/common/160 duplicate source names/dir1/file.c
new file mode 100644
--- /dev/null
+++ b/test cases/common/160 duplicate source names/dir1/file.c	
@@ -0,0 +1,16 @@
+extern int dir2;
+extern int dir2_dir1;
+extern int dir3;
+extern int dir3_dir1;
+
+int main() {
+    if (dir2 != 20)
+        return 1;
+    if (dir2_dir1 != 21)
+        return 1;
+    if (dir3 != 30)
+        return 1;
+    if (dir3_dir1 != 31)
+        return 1;
+    return 0;
+}
diff --git a/test cases/common/160 duplicate source names/dir1/meson.build b/test cases/common/160 duplicate source names/dir1/meson.build
new file mode 100644
--- /dev/null
+++ b/test cases/common/160 duplicate source names/dir1/meson.build	
@@ -0,0 +1 @@
+sources += files('file.c')
diff --git a/test cases/common/160 duplicate source names/dir2/dir1/file.c b/test cases/common/160 duplicate source names/dir2/dir1/file.c
new file mode 100644
--- /dev/null
+++ b/test cases/common/160 duplicate source names/dir2/dir1/file.c	
@@ -0,0 +1 @@
+int dir2_dir1 = 21;
diff --git a/test cases/common/160 duplicate source names/dir2/file.c b/test cases/common/160 duplicate source names/dir2/file.c
new file mode 100644
--- /dev/null
+++ b/test cases/common/160 duplicate source names/dir2/file.c	
@@ -0,0 +1 @@
+int dir2 = 20;
diff --git a/test cases/common/160 duplicate source names/dir2/meson.build b/test cases/common/160 duplicate source names/dir2/meson.build
new file mode 100644
--- /dev/null
+++ b/test cases/common/160 duplicate source names/dir2/meson.build	
@@ -0,0 +1 @@
+sources += files('file.c', 'dir1/file.c')
diff --git a/test cases/common/160 duplicate source names/dir3/dir1/file.c b/test cases/common/160 duplicate source names/dir3/dir1/file.c
new file mode 100644
--- /dev/null
+++ b/test cases/common/160 duplicate source names/dir3/dir1/file.c	
@@ -0,0 +1 @@
+int dir3_dir1 = 31;
diff --git a/test cases/common/160 duplicate source names/dir3/file.c b/test cases/common/160 duplicate source names/dir3/file.c
new file mode 100644
--- /dev/null
+++ b/test cases/common/160 duplicate source names/dir3/file.c	
@@ -0,0 +1 @@
+int dir3 = 30;
diff --git a/test cases/common/160 duplicate source names/dir3/meson.build b/test cases/common/160 duplicate source names/dir3/meson.build
new file mode 100644
--- /dev/null
+++ b/test cases/common/160 duplicate source names/dir3/meson.build	
@@ -0,0 +1 @@
+lib = static_library('lib', 'file.c', 'dir1/file.c')
diff --git a/test cases/common/160 duplicate source names/meson.build b/test cases/common/160 duplicate source names/meson.build
new file mode 100644
--- /dev/null
+++ b/test cases/common/160 duplicate source names/meson.build	
@@ -0,0 +1,7 @@
+project('proj', 'c')
+
+sources = []
+subdir('dir1')
+subdir('dir2')
+subdir('dir3')
+executable('a.out', sources : sources, objects : lib.extract_all_objects())