Repository: mesonbuild/meson

Issue:
Spurious "multiple executables with the same name" and other issues with #11355
Hi,

The warning introduced in #11355 triggers, I assume unintentionally, when having two targets with the same name *in different directories*, rather than just when in the same directory.  I think the test would need to check not just the target name but also the that the target subdir is the same.

Separately, the warning also seems to not have enough detail to easily tackle - neither executable's name isn't mentioned, making it hard to know what the conflict is. One can possibly be inferred from the line (although not always, consider a foreach loop), but the other can't.  I'd suggest adding at least the current and the conflicting target names, including the differing suffixes, to the warning.

Thirdly, I think the code changes added an O(num_target^2) behavior. Previously the test for conflicting targets was a dictionary membership test (effectively O(1), thus N target are O(N)). But now the conflicts are tested by iterating over all existing targets - once for each target.  And indeed: I made a test project that creates 10k targets using range(). On adb1a360b^ it takes 16s, on adb1a360b 1m11.573s.

Note that this isn't just a "CS purity" style concern - reconfiguring postgres goes from ~5.30s to ~6.70s before/after adb1a360b.

[repro.tar.gz](https://github.com/mesonbuild/meson/files/13057856/repro.tar.gz)

CC: @Dudemanguy 

Greetings,

Andres




Discussion:
Yup looking at this again, the warning is missing an extra condition hence why you are getting those messages. Also the loop shouldn't really be needed since this was already a dictionary.
@Dudemanguy I think the reason you turned the dict lookup into a loop is that the dict is keyed by the id, not the target name. As you now want to look up not just by id, but *also* by the "unsuffixed" target name, you can't use the efficient dictionary lookup. 

I guess you could build a separate dict that's keyed by `(directory, target_name)` , but given that that's not needed anywhere else so far...

A different way to fix the issue could be to only add the name suffix to the id in `Target.get_id` if the minimum version is >= 1.3, that way you wouldn't need any additional checks.
I'm thinking just saving the names in a separate list or dictionary indeed.

>A different way to fix the issue could be to only add the name suffix to the id in Target.get_id if the minimum version is >= 1.3, that way you wouldn't need any additional checks.

I don't think that would work? The feature is still supposed to work if you don't set a minimum version in the project, but you would get a warning like with other meson things.
> I'm thinking just saving the names in a separate list or dictionary indeed.

A list would just have the same O(N^2) issue :)


> >  A different way to fix the issue could be to only add the name suffix to the id in Target.get_id if the minimum version is >= 1.3, that way you wouldn't need any additional checks.

> I don't think that would work? The feature is still supposed to work if you don't set a minimum version in the project, but you would get a warning like with other meson things.

No minimum version is basically the same as using the current version as the minimum version, no? You certainly don't get a warning about the conflicting names today if you don't set a minimum version - I know because I was temporarily confused why I wasn't seeing the spurious warning in my repro, before I realized I forgot the minimum version...
>A list would just have the same O(N^2) issue :)

Set not list. ;).

Edit: Nevermind sets aren't ordered apparently so maybe just a dictionary after all.

>No minimum version is basically the same as using the current version as the minimum version, no?

Yeah that was wrong... What I actually meant was if you set the minimum below 1.3.0 but still try to use the feature.
In general, meson refrains from "policies" -- cases where you state in the build files prologue (project function) whether to use an old style or a new style approach to interpreting the build and generating different build.ninja as a result.

Project minimum versions are purely advisory and only affect which messages are printed. In future we may simply default it to a version you didn't expect but which we feel makes for a good "report old features if you didn't specify" range.

Resolving patch:
diff --git a/mesonbuild/build.py b/mesonbuild/build.py
--- a/mesonbuild/build.py
+++ b/mesonbuild/build.py
@@ -252,6 +252,7 @@ def __init__(self, environment: environment.Environment):
         self.environment = environment
         self.projects = {}
         self.targets: 'T.OrderedDict[str, T.Union[CustomTarget, BuildTarget]]' = OrderedDict()
+        self.targetnames: T.Set[T.Tuple[str, str]] = set() # Set of executable names and their subdir
         self.global_args: PerMachine[T.Dict[str, T.List[str]]] = PerMachine({}, {})
         self.global_link_args: PerMachine[T.Dict[str, T.List[str]]] = PerMachine({}, {})
         self.projects_args: PerMachine[T.Dict[str, T.Dict[str, T.List[str]]]] = PerMachine({}, {})
diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py
--- a/mesonbuild/interpreter/interpreter.py
+++ b/mesonbuild/interpreter/interpreter.py
@@ -3163,12 +3163,15 @@ def add_target(self, name: str, tobj: build.Target) -> None:
         # To permit an executable and a shared library to have the
         # same name, such as "foo.exe" and "libfoo.a".
         idname = tobj.get_id()
-        for t in self.build.targets.values():
-            if t.get_id() == idname:
-                raise InvalidCode(f'Tried to create target "{name}", but a target of that name already exists.')
-            if isinstance(tobj, build.Executable) and isinstance(t, build.Executable) and t.name == tobj.name:
-                FeatureNew.single_use('multiple executables with the same name but different suffixes',
-                                      '1.3.0', self.subproject, location=self.current_node)
+        subdir = tobj.get_subdir()
+        namedir = (name, subdir)
+
+        if idname in self.build.targets:
+            raise InvalidCode(f'Tried to create target "{name}", but a target of that name already exists.')
+
+        if isinstance(tobj, build.Executable) and namedir in self.build.targetnames:
+            FeatureNew.single_use(f'multiple executables with the same name, "{tobj.name}", but different suffixes in the same directory',
+                                  '1.3.0', self.subproject, location=self.current_node)
 
         if isinstance(tobj, build.BuildTarget):
             self.add_languages(tobj.missing_languages, True, tobj.for_machine)
@@ -3176,6 +3179,9 @@ def add_target(self, name: str, tobj: build.Target) -> None:
             self.add_stdlib_info(tobj)
 
         self.build.targets[idname] = tobj
+        # Only need to add executables to this set
+        if isinstance(tobj, build.Executable):
+            self.build.targetnames.update([namedir])
         if idname not in self.coredata.target_guids:
             self.coredata.target_guids[idname] = str(uuid.uuid4()).upper()
 


Test patch: