Repository: conan-io/conan

Issue:
Outdated dep does not trigger rebuild
conan ver: 1.15.0
os: Ubuntu 17.04 x86_64

I have a **myapp** (executable) pkg which requires a **mylib** (static lib) pkg.
1. **mylib** has a bug fixed. The pkg is exported to the local cache.
2. I `conan create` a **myapp** pkg with `-b outdated`.
3. The **mylib** is rebuilt properly as a dependency of **myapp** since there are new sources. The **mylib** pkg id does not change since there are no changes in the settings or options.
4. **myapp** is not rebuild since the dependency's id did not change. Conan says it's `Already installed!`.

I would expect that a rebuilt dependency would trigger a rebuild of any pkgs that depend upon it. Am I missing something?

I don't want to bump the version of the lib after each commit or trigger the rebuild of the app manually.



- [x] I've read the [CONTRIBUTING guide](https://github.com/conan-io/conan/blob/develop/.github/CONTRIBUTING.md).
- [x] I've specified the Conan version, operating system version and any tool that can be relevant.
- [x] I've explained the steps to reproduce the error or the motivation/use case of the question/suggestion.




Discussion:
hi there,
if only the code changes then Conan does not know that it has changed and does not initiate a rebuild.
If the recipe has changed then Conan does a rebuild.
If the calculated ```package_id``` of your recipe configuration does not exist Conan initiates a build.
The ```package_id``` is based on the provided settings and options of your recipe at invocation of Conan with your recipe ([see here](https://docs.conan.io/en/latest/reference/conanfile/methods.html#package-id)).

You can instruct Conan to rebuild a specific dependency with the ```build``` parameter.

```conan install --build=outdated --build=mylib ...```
> if only the code changes then Conan does not know that it has changed and does not initiate a rebuild.

This is not what I experience. I export the **mylib** pkg to the local cache with just code changes. I create a package that depends (with `-b outdated`) on it and it **gets rebuild**, which is correct in my opinion.

The thing is that the package that has its dependency rebuild **does not get rebuilt** - this, I think, is a bug.

I don't want to manually trigger rebuilds of pkgs when their dependencies got rebuilt. This should be done automatically.
Ah ok. I see.
If it is in the same repository this should already be the correct behaviour to rebuild depending packages.

You did not mention if you pull the ```mylib``` from the same or some remote repository.
But if you use some repository from a remote you then you can use the ```--update``` parameter on ```conan install``` as this checks the remote repositories if there are updates to the recipes/packages.
Let me clarify:
1. There is no conan server (remote) involved. Just the local cache.
2. **mylib/1.2.3@user/channel** -- a C++ static lib, packaged with conan.
3. **myapp/4.5.6@user/channel** -- a C++ application, packaged with conan, requires **mylib/1.2.3@user/channel**.

The issue:
1. I do **only** code changes to **mylib**.
2. I do `conan export <mylib-path> user/channel`. New source is moved to the local cache.
3. I want to rebuild **myapp** since the dependency changed.
4. I do `conan create <myapp-path> user/channel -b outdated`.
5. conan resolves the dependencies: notices that **mylib/1.2.3@user/channel** has new source. **mylib** gets rebuild -- this is what I expect.
6. After this conan declares that **myapp/4.5.6@user/channel** is **Already installed!** and does not build my application with the updated dependency. This means that the `conan create ...` from point 4. only rebuilds my dep but not the pkg that depends upon it. IMHO this is not correct
A change with a single comment in the recipe will make it outdated too, and it doesn't mean that the package of the application should be rebuilt. The same if the library is shared and no header has changed. To let Conan know that the application has to be rebuilt the `package_id` of the application has to change. 

Check [default_package_id_mode](https://docs.conan.io/en/latest/creating_packages/define_abi_compatibility.html#changing-the-default-package-id-mode).

If you are not bumping the version probably you are interested in the [revisions](https://docs.conan.io/en/latest/mastering/revisions.html) feature. We are working [here](https://github.com/conan-io/conan/pull/4958) on a new mode that will change the package_id of the application even if the hash of the package of `mylib` changes.

We currently don't have a build policy meaning `build outdated and all the downstream nodes`. If you are interested in the automation of a CI process, we could provide some ideas. The `--json` output of the `conan info` could help, also the `--build-order` that will tell you which libraries would be affected by the rebuilding of the library.

EDIT: I agree that if you are not thinking about CI and/or you are ok rebuilding everything in the same process, it is far from being ideal. I will talk with the team to discuss if an argument to say "build also the downstream deps of a rebuilt one" would make sense and how complex could be.
Unfortunately neither of those solutions is good enough for me. I'm trying to automate a build system and I need to know when a rebuild is necessary.

Currently, we are bumping the version of the dependency with each commit but that is really annoying since we need to update the version of the dep and the required fields of all the pkgs that use it. It blows up really fast and requires a lot of manual intervention and it's easy to forget to update a version somewhere.

Can you suggest a way to solve this issue?
The way to solve it would be to implement a new [build policy](https://docs.conan.io/en/latest/mastering/policies.html) to build the nodes to build and all the downstream nodes.
I think this would be a very interesting thing to have. Thanks, we will try to move it forward. 
Possible approach:

`--build=outdated --build=recursive` (or similar name), so if a node has to be built (because of whatever policy) if the new `--build=recursive` is specified, it will cause any node depending on another node built, to be built.

Resolving patch:
diff --git a/conans/client/graph/build_mode.py b/conans/client/graph/build_mode.py
--- a/conans/client/graph/build_mode.py
+++ b/conans/client/graph/build_mode.py
@@ -16,6 +16,7 @@ def __init__(self, params, output):
         self.outdated = False
         self.missing = False
         self.never = False
+        self.cascade = False
         self.patterns = []
         self._unused_patterns = []
         self.all = False
@@ -33,14 +34,16 @@ def __init__(self, params, output):
                     self.missing = True
                 elif param == "never":
                     self.never = True
+                elif param == "cascade":
+                    self.cascade = True
                 else:
                     self.patterns.append("%s" % param)
 
-            if self.never and (self.outdated or self.missing or self.patterns):
+            if self.never and (self.outdated or self.missing or self.patterns or self.cascade):
                 raise ConanException("--build=never not compatible with other options")
         self._unused_patterns = list(self.patterns)
 
-    def forced(self, conan_file, ref):
+    def forced(self, conan_file, ref, with_deps_to_build=False):
         if self.never:
             return False
         if self.all:
@@ -51,6 +54,9 @@ def forced(self, conan_file, ref):
                                    "build_policy='always'")
             return True
 
+        if self.cascade and with_deps_to_build:
+            return True
+
         # Patterns to match, if package matches pattern, build is forced
         for pattern in self.patterns:
             is_matching_name = fnmatch.fnmatchcase(ref.name, pattern)
diff --git a/conans/client/graph/graph_binaries.py b/conans/client/graph/graph_binaries.py
--- a/conans/client/graph/graph_binaries.py
+++ b/conans/client/graph/graph_binaries.py
@@ -4,7 +4,7 @@
                                        BINARY_SKIP, BINARY_UPDATE,
                                        RECIPE_EDITABLE, BINARY_EDITABLE,
                                        RECIPE_CONSUMER, RECIPE_VIRTUAL)
-from conans.errors import NoRemoteAvailable, NotFoundException,\
+from conans.errors import NoRemoteAvailable, NotFoundException, \
     conanfile_exception_formatter
 from conans.model.info import ConanInfo
 from conans.model.manifest import FileTreeManifest
@@ -54,7 +54,8 @@ def _evaluate_node(self, node, build_mode, update, evaluated_nodes, remotes):
             # TODO: PREV?
             return
 
-        if build_mode.forced(conanfile, ref):
+        with_deps_to_build = any([dep.dst.binary == BINARY_BUILD for dep in node.dependencies])
+        if build_mode.forced(conanfile, ref, with_deps_to_build):
             output.info('Forced build from source')
             node.binary = BINARY_BUILD
             node.prev = None


Test patch:
diff --git a/conans/test/integration/install_cascade_test.py b/conans/test/integration/install_cascade_test.py
new file mode 100644
--- /dev/null
+++ b/conans/test/integration/install_cascade_test.py
@@ -0,0 +1,81 @@
+import unittest
+from collections import OrderedDict
+
+from conans.model.ref import ConanFileReference
+
+from conans.test.utils.tools import TestServer, TurboTestClient, GenConanfile
+
+
+class InstallCascadeTest(unittest.TestCase):
+
+    def setUp(self):
+        """
+         A
+        / \
+       B   C
+       |    \
+       D    |
+      / \   |
+      |  \ /
+      E   F
+        """
+        server = TestServer()
+        servers = OrderedDict([("default", server)])
+        self.client = TurboTestClient(servers=servers)
+
+        self.ref_a = ConanFileReference.loads("libA/1.0@conan/stable")
+        self.client.create(self.ref_a, conanfile=GenConanfile())
+
+        self.ref_b = ConanFileReference.loads("libB/1.0@conan/stable")
+        self.client.create(self.ref_b, conanfile=GenConanfile().with_requirement(self.ref_a))
+
+        self.ref_c = ConanFileReference.loads("libC/1.0@conan/stable")
+        self.client.create(self.ref_c, conanfile=GenConanfile().with_requirement(self.ref_a))
+
+        self.ref_d = ConanFileReference.loads("libD/1.0@conan/stable")
+        self.client.create(self.ref_d, conanfile=GenConanfile().with_requirement(self.ref_b))
+
+        self.ref_e = ConanFileReference.loads("libE/1.0@conan/stable")
+        self.client.create(self.ref_e, conanfile=GenConanfile().with_requirement(self.ref_d))
+
+        self.ref_f = ConanFileReference.loads("libF/1.0@conan/stable")
+        conanfile = GenConanfile().with_requirement(self.ref_c).with_requirement(self.ref_d)
+        self.client.create(self.ref_f, conanfile=conanfile)
+
+    def _assert_built(self, refs):
+        for ref in refs:
+            self.assertIn("{}: Copying sources to build folder".format(ref), self.client.out)
+        for ref in [self.ref_a, self.ref_b, self.ref_c, self.ref_d, self.ref_e, self.ref_f]:
+            if ref not in refs:
+                self.assertNotIn("{}: Copying sources to build folder".format(ref),
+                                 self.client.out)
+
+    def install_cascade_only_affected_test(self):
+        project = ConanFileReference.loads("project/1.0@conan/stable")
+        project_cf = GenConanfile().with_requirement(self.ref_e).with_requirement(self.ref_f)
+
+        # Building A everything is built
+        self.client.create(project, conanfile=project_cf,
+                           args="--build {} --build cascade".format(self.ref_a))
+        self._assert_built([self.ref_a, self.ref_b, self.ref_c, self.ref_d,
+                            self.ref_e, self.ref_f, project])
+
+        # Building D builds E, F and project
+        self.client.create(project, conanfile=project_cf,
+                           args="--build {} --build cascade".format(self.ref_d))
+        self._assert_built([self.ref_d, self.ref_e, self.ref_f, project])
+
+        # Building E only builds E and project
+        self.client.create(project, conanfile=project_cf,
+                           args="--build {} --build cascade".format(self.ref_e))
+        self._assert_built([self.ref_e, project])
+
+        # Building project only builds project
+        self.client.create(project, conanfile=project_cf,
+                           args="--build {} --build cascade".format(project))
+        self._assert_built([project])
+
+        # Building C => builds F and project
+        self.client.create(project, conanfile=project_cf,
+                           args="--build {} --build cascade".format(self.ref_c))
+        self._assert_built([project, self.ref_f, self.ref_c])
diff --git a/conans/test/unittests/client/graph/build_mode_test.py b/conans/test/unittests/client/graph/build_mode_test.py
--- a/conans/test/unittests/client/graph/build_mode_test.py
+++ b/conans/test/unittests/client/graph/build_mode_test.py
@@ -20,16 +20,25 @@ def test_valid_params(self):
         self.assertTrue(build_mode.outdated)
         self.assertTrue(build_mode.missing)
         self.assertFalse(build_mode.never)
+        self.assertFalse(build_mode.cascade)
 
         build_mode = BuildMode(["never"], self.output)
         self.assertFalse(build_mode.outdated)
         self.assertFalse(build_mode.missing)
         self.assertTrue(build_mode.never)
+        self.assertFalse(build_mode.cascade)
+
+        build_mode = BuildMode(["cascade"], self.output)
+        self.assertFalse(build_mode.outdated)
+        self.assertFalse(build_mode.missing)
+        self.assertFalse(build_mode.never)
+        self.assertTrue(build_mode.cascade)
 
     def test_invalid_configuration(self):
-        with six.assertRaisesRegex(self, ConanException,
-                                     "--build=never not compatible with other options"):
-            BuildMode(["outdated", "missing", "never"], self.output)
+        for mode in ["outdated", "missing", "cascade"]:
+            with six.assertRaisesRegex(self, ConanException,
+                                         "--build=never not compatible with other options"):
+                BuildMode([mode, "never"], self.output)
 
     def test_common_build_force(self):
         reference = ConanFileReference.loads("Hello/0.1@user/testing")