Repository: mesonbuild/meson

Issue:
AttributeError: 'SubprojectHolder' object has no attribute 'd_features'
**Describe the bug**
Attempting to use `libjpeg-turbo` from `wrapdb` as a dependency for my project. During compilation, I get told that the above-mentioned error should be reported. As a law-abiding developer, I humbly oblige.

**To Reproduce**
```meson
project(
  'meson_wrap_reproduct',
  'c',
  version: '0.1',
  default_options: [
    'c_std=18',
    'warning_level=3',
    'werror=true'
  ],
)

libjpeg = subproject('libjpeg-turbo')

executable(
  'meson_wrap_reproduct',
  'meson_wrap_reproduct.c',
  dependencies: [
    libjpeg,
  ]
)
```

with `libjpeg-turbo` as a subprojects dependency.

**Expected behavior**
Using the dependency from `wrapdb` should work.

**system parameters**
* Is this a [cross build](https://mesonbuild.com/Cross-compilation.html) or just a plain native build (for the same computer)?
  * Plain
* what operating system (e.g. MacOS Catalina, Windows 10, CentOS 8.0, Ubuntu 18.04, etc.)
  * Arch Linux @ 5.17.9
* what Python version are you using e.g. 3.8.0
  * 3.10.4
* what `meson --version`
  * 0.62.2
* what `ninja --version` if it's a Ninja build
  * 1.10.2

This seems to be the case for `libpng` as well, so I'm assuming this is a wider problem.


Discussion:
```meson
libjpeg = subproject('libjpeg-turbo')

executable(
     ....
  dependencies: [
    libjpeg,
  ]
```

This is a sanity checking bug, because Meson should not error out with a 46-line traceback which users don't understand, but should instead emit a very simple error that clearly tells users what the problem is, of the form:

```
meson.build:14:0: ERROR: executable keyword argument 'dependencies' was of type array[SubprojectHolder] but should have been array[actually | permitted | types]
```



In the long term this will be fixed by #9950 which will give us a sensible way to generically constrain this.

In the short term we can add a specific type validation here, which we already do for lots of other things for the executable() function.
> ```meson
> libjpeg = subproject('libjpeg-turbo')
> ```

Try instead `dependency('libjpeg')`, it will check if libjpeg is found on system first, then fallback to use libjpeg-turbo from wrapdb assuming you have `subprojects/libjpeg-turbo.wrap`.
That sounds amazing @xclaesse! 😍

I currently have to specify `'werror=false'` in my `default_options` for both `libjpeg-turbo` and `libpng`, since I am using it in my own project `default_options`. How do I ensure that continues (as they won't build without) if I use `dependency(...)` instead?
Both `subproject()` and `dependency()` accept a `default_options: ` kwarg, and in both cases its usage is that it will be passed to a subproject -- in the former case, explicitly so; in the latter case, whenever the dependency lookup triggers a subproject fallback.

Note also: you can prefer the subproject fallback even on systems where libjpeg-turbo is globally installed (e.g. to create standalone executables) by using `meson setup --wrap-mode=forcefallback`.
If you really, really feel you must use the subproject explicitly and not leave any option to first find a prebuilt system copy, that works too, but you need to use the returned subproject object `libjpeg` like this:

```
libjpeg_dep = libjpeg.get_variable('jpeg_dep')

executable(..., dependencies: libjpeg_dep)
```

get_variable() is how you manually retrieve e.g. a "dependency" object (assigned in the libjpeg-turbo subproject as `jpeg_dep = declare_dependency(...)`) from a subproject object container.

The dependency fallback is a lot more elegant if we assume that the project doesn't care how it gets libjpeg. It generally doesn't matter except to the person building a specific binary artifact.
@eli-schwartz, thanks for the insight! I should probably read the Meson Manual again 😄

Have an excellent evening both of you!

Resolving patch:
diff --git a/mesonbuild/build.py b/mesonbuild/build.py
--- a/mesonbuild/build.py
+++ b/mesonbuild/build.py
@@ -1343,12 +1343,6 @@ def add_deps(self, deps):
             if dep in self.added_deps:
                 continue
 
-            dep_d_features = dep.d_features
-
-            for feature in ('versions', 'import_dirs'):
-                if feature in dep_d_features:
-                    self.d_features[feature].extend(dep_d_features[feature])
-
             if isinstance(dep, dependencies.InternalDependency):
                 # Those parts that are internal.
                 self.process_sourcelist(dep.sources)
@@ -1379,6 +1373,9 @@ def add_deps(self, deps):
                 # This is a bit of a hack. We do not want Build to know anything
                 # about the interpreter so we can't import it and use isinstance.
                 # This should be reliable enough.
+                if hasattr(dep, 'held_object'):
+                    # FIXME: subproject is not a real ObjectHolder so we have to do this by hand
+                    dep = dep.held_object
                 if hasattr(dep, 'project_args_frozen') or hasattr(dep, 'global_args_frozen'):
                     raise InvalidArguments('Tried to use subproject object as a dependency.\n'
                                            'You probably wanted to use a dependency declared in it instead.\n'
@@ -1387,6 +1384,13 @@ def add_deps(self, deps):
                                        'either an external dependency (returned by find_library() or '
                                        'dependency()) or an internal dependency (returned by '
                                        'declare_dependency()).')
+
+            dep_d_features = dep.d_features
+
+            for feature in ('versions', 'import_dirs'):
+                if feature in dep_d_features:
+                    self.d_features[feature].extend(dep_d_features[feature])
+
             self.added_deps.add(dep)
 
     def get_external_deps(self) -> T.List[dependencies.Dependency]:


Test patch:
diff --git a/test cases/failing/127 subproject object as a dependency/main.c b/test cases/failing/127 subproject object as a dependency/main.c
new file mode 100644
--- /dev/null
+++ b/test cases/failing/127 subproject object as a dependency/main.c	
@@ -0,0 +1 @@
+int main(void) { return 0; }
diff --git a/test cases/failing/127 subproject object as a dependency/meson.build b/test cases/failing/127 subproject object as a dependency/meson.build
new file mode 100644
--- /dev/null
+++ b/test cases/failing/127 subproject object as a dependency/meson.build	
@@ -0,0 +1,4 @@
+project('test', 'c')
+
+executable(
+  'main', 'main.c', dependencies: subproject('sub'))
diff --git a/test cases/failing/127 subproject object as a dependency/subprojects/sub/meson.build b/test cases/failing/127 subproject object as a dependency/subprojects/sub/meson.build
new file mode 100644
--- /dev/null
+++ b/test cases/failing/127 subproject object as a dependency/subprojects/sub/meson.build	
@@ -0,0 +1 @@
+project('sub')
diff --git a/test cases/failing/127 subproject object as a dependency/test.json b/test cases/failing/127 subproject object as a dependency/test.json
new file mode 100644
--- /dev/null
+++ b/test cases/failing/127 subproject object as a dependency/test.json	
@@ -0,0 +1,7 @@
+{
+  "stdout": [
+    {
+      "line": "test cases/failing/127 subproject object as a dependency/meson.build:3:0: ERROR: Tried to use subproject object as a dependency."
+    }
+  ]
+}