Repository: conan-io/conan

Issue:
[question] Is there a way to skip specific packages from build when calling install --build?
I've built a package separately and it took a lot of time. I want to rebuild other main project's dependencies but pass a specific package. Is it possible?

**UPD**: Or even better, an option that will force build packages fetched from remote but skip build if the package has already been built in local cache.
Example: I build qt from sources which takes a lot of time, then it is built and ready in my cache. I run `conan install` in my application project and it fetches another packages and builds it from sources skipping the qt

- [x] I've read the [CONTRIBUTING guide](https://github.com/conan-io/conan/blob/develop/.github/CONTRIBUTING.md).


Discussion:
Hi @xahon,
If you want to skip building already built packages you should pass the `missing` parameter to `--build` argument when calling `conan install`. If you just call `--build` Conan will try to rebuild all the dependencies from sources. If you already built that package for the same configuration that you are using in the consumer project, then it should not be built but reused from the Conan cache. Please have a look at the [Conan build policies](https://docs.conan.io/en/latest/mastering/policies.html).
Thanks a lot for your question, I hope this helps.
Hi @xahon,
I think I misunderstood your question. Do you mean that you want to build everything but an specific package?
Something like: `conan install --build --build=!mypkg`
If that's the case that's not possible currently in Conan but could be a valid feature request.
@czoido Hi! Yes, I meant especially this. I want to rebuild "everything" except "something". Could you transform the issue into a feature request?

Resolving patch:
diff --git a/conans/client/command.py b/conans/client/command.py
--- a/conans/client/command.py
+++ b/conans/client/command.py
@@ -40,7 +40,7 @@ class Extender(argparse.Action):
           settings = ['cucumber:true']
     """
     def __call__(self, parser, namespace, values, option_strings=None):  # @UnusedVariable
-        # Need None here incase `argparse.SUPPRESS` was supplied for `dest`
+        # Need None here in case `argparse.SUPPRESS` was supplied for `dest`
         dest = getattr(namespace, self.dest, None)
         if not hasattr(dest, 'extend') or dest == self.default:
             dest = []
@@ -2282,6 +2282,8 @@ def settings_args(machine, short_suffix="", long_suffix=""):
                        source.
     --build=[pattern]  Build packages from source whose package reference matches the pattern. The
                        pattern uses 'fnmatch' style wildcards.
+    --build=![pattern] Excluded packages, which will not be built from the source, whose package
+                       reference matches the pattern. The pattern uses 'fnmatch' style wildcards.
 
     Default behavior: If you omit the '--build' option, the 'build_policy' attribute in conanfile.py
     will be used if it exists, otherwise the behavior is like '--build={}'.
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
@@ -10,6 +10,7 @@ class BuildMode(object):
                    => False if user wrote "never"
                    => True if user wrote "missing"
                    => "outdated" if user wrote "--build outdated"
+                   => ["!foo"] means exclude when building all from sources
     """
     def __init__(self, params, output):
         self._out = output
@@ -19,6 +20,7 @@ def __init__(self, params, output):
         self.cascade = False
         self.patterns = []
         self._unused_patterns = []
+        self._excluded_patterns = []
         self.all = False
         if params is None:
             return
@@ -40,13 +42,30 @@ def __init__(self, params, output):
                     # Remove the @ at the end, to match for "conan install pkg/0.1@ --build=pkg/0.1@"
                     clean_pattern = param[:-1] if param.endswith("@") else param
                     clean_pattern = clean_pattern.replace("@#", "#")
-                    self.patterns.append(clean_pattern)
+                    if clean_pattern and clean_pattern[0] == "!":
+                        self._excluded_patterns.append(clean_pattern[1:])
+                    else:
+                        self.patterns.append(clean_pattern)
 
             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)
+        self._unused_patterns = list(self.patterns) + self._excluded_patterns
 
     def forced(self, conan_file, ref, with_deps_to_build=False):
+        def pattern_match(pattern_):
+            return (fnmatch.fnmatchcase(ref.name, pattern_) or
+                    fnmatch.fnmatchcase(repr(ref.copy_clear_rev()), pattern_) or
+                    fnmatch.fnmatchcase(repr(ref), pattern_))
+
+        for pattern in self._excluded_patterns:
+            if pattern_match(pattern):
+                try:
+                    self._unused_patterns.remove(pattern)
+                except ValueError:
+                    pass
+                conan_file.output.info("Excluded build from source")
+                return False
+
         if self.never:
             return False
         if self.all:
@@ -62,9 +81,7 @@ def forced(self, conan_file, ref, with_deps_to_build=False):
 
         # Patterns to match, if package matches pattern, build is forced
         for pattern in self.patterns:
-            if (fnmatch.fnmatchcase(ref.name, pattern) or
-                    fnmatch.fnmatchcase(repr(ref.copy_clear_rev()), pattern) or
-                    fnmatch.fnmatchcase(repr(ref), pattern)):
+            if pattern_match(pattern):
                 try:
                     self._unused_patterns.remove(pattern)
                 except ValueError:
@@ -83,4 +100,4 @@ def allowed(self, conan_file):
 
     def report_matches(self):
         for pattern in self._unused_patterns:
-            self._out.error("No package matching '%s' pattern" % pattern)
+            self._out.error("No package matching '%s' pattern found." % pattern)


Test patch:
diff --git a/conans/test/functional/graph/test_graph_build_mode.py b/conans/test/functional/graph/test_graph_build_mode.py
new file mode 100644
--- /dev/null
+++ b/conans/test/functional/graph/test_graph_build_mode.py
@@ -0,0 +1,166 @@
+import pytest
+from conans.test.assets.genconanfile import GenConanfile
+from conans.test.utils.tools import TestClient
+
+
+@pytest.fixture(scope="module")
+def build_all():
+    """ Build a simple graph to test --build option
+
+        foobar <- bar <- foo
+               <--------|
+
+        All packages are built from sources to keep a cache.
+    :return: TestClient instance
+    """
+    client = TestClient()
+    client.save({"conanfile.py": GenConanfile().with_setting("build_type")})
+    client.run("export . foo/1.0@user/testing")
+    client.save({"conanfile.py": GenConanfile().with_require("foo/1.0@user/testing")
+                .with_setting("build_type")})
+    client.run("export . bar/1.0@user/testing")
+    client.save({"conanfile.py": GenConanfile().with_require("foo/1.0@user/testing")
+                .with_require("bar/1.0@user/testing")
+                .with_setting("build_type")})
+    client.run("export . foobar/1.0@user/testing")
+    client.run("install foobar/1.0@user/testing --build")
+
+    return client
+
+
+def check_if_build_from_sources(refs_modes, output):
+    for ref, mode in refs_modes.items():
+        if mode == "Build":
+            assert "{}/1.0@user/testing: Forced build from source".format(ref) in output
+        else:
+            assert "{}/1.0@user/testing: Forced build from source".format(ref) not in output
+
+
+def test_install_build_single(build_all):
+    """ When only --build=<ref> is passed, only <ref> must be built
+    """
+    build_all.run("install foobar/1.0@user/testing --build=foo")
+
+    assert "bar/1.0@user/testing:7839863d5a059fc6579f28026763e1021268c55e - Cache" in build_all.out
+    assert "foo/1.0@user/testing:4024617540c4f240a6a5e8911b0de9ef38a11a72 - Build" in build_all.out
+    assert "foobar/1.0@user/testing:89636fbae346e3983af2dd63f2c5246505e74be7 - Cache" in build_all.out
+    assert "foo/1.0@user/testing: Forced build from source" in build_all.out
+    assert "bar/1.0@user/testing: Forced build from source" not in build_all.out
+    assert "foobar/1.0@user/testing: Forced build from source" not in build_all.out
+    assert "No package matching" not in build_all.out
+
+
+def test_install_build_double(build_all):
+    """ When both --build=<ref1> and --build=<ref2> are passed, only both should be built
+    """
+    build_all.run("install foobar/1.0@user/testing --build=foo --build=bar")
+
+    assert "bar/1.0@user/testing:7839863d5a059fc6579f28026763e1021268c55e - Build" in build_all.out
+    assert "foo/1.0@user/testing:4024617540c4f240a6a5e8911b0de9ef38a11a72 - Build" in build_all.out
+    assert "foobar/1.0@user/testing:89636fbae346e3983af2dd63f2c5246505e74be7 - Cache" in build_all.out
+    assert "foo/1.0@user/testing: Forced build from source" in build_all.out
+    assert "bar/1.0@user/testing: Forced build from source" in build_all.out
+    assert "foobar/1.0@user/testing: Forced build from source" not in build_all.out
+    assert "No package matching" not in build_all.out
+
+
+@pytest.mark.parametrize("build_arg,mode", [("--build", "Build"),
+                                            ("--build=", "Cache"),
+                                            ("--build=*", "Build")])
+def test_install_build_only(build_arg, mode, build_all):
+    """ When only --build is passed, all packages must be built from sources
+        When only --build= is passed, it's considered an error
+        When only --build=* is passed, all packages must be built from sources
+    """
+    build_all.run("install foobar/1.0@user/testing {}".format(build_arg))
+
+    assert "bar/1.0@user/testing:7839863d5a059fc6579f28026763e1021268c55e - {}".format(mode) in build_all.out
+    assert "foo/1.0@user/testing:4024617540c4f240a6a5e8911b0de9ef38a11a72 - {}".format(mode) in build_all.out
+    assert "foobar/1.0@user/testing:89636fbae346e3983af2dd63f2c5246505e74be7 - {}".format(mode) in build_all.out
+
+    if "Build" == mode:
+        assert "foo/1.0@user/testing: Forced build from source" in build_all.out
+        assert "bar/1.0@user/testing: Forced build from source" in build_all.out
+        assert "foobar/1.0@user/testing: Forced build from source" in build_all.out
+        assert "No package matching" not in build_all.out
+    else:
+        assert "foo/1.0@user/testing: Forced build from source" not in build_all.out
+        assert "bar/1.0@user/testing: Forced build from source" not in build_all.out
+        assert "foobar/1.0@user/testing: Forced build from source" not in build_all.out
+        assert "No package matching" in build_all.out
+
+
+@pytest.mark.parametrize("build_arg,bar,foo,foobar", [("--build", "Cache", "Build", "Cache"),
+                                                      ("--build=", "Cache", "Build", "Cache"),
+                                                      ("--build=*", "Build", "Build", "Build")])
+def test_install_build_all_with_single(build_arg, bar, foo, foobar, build_all):
+    """ When --build is passed with another package, only the package must be built from sources.
+        When --build= is passed with another package, only the package must be built from sources.
+        When --build=* is passed with another package, all packages must be built from sources.
+    """
+    build_all.run("install foobar/1.0@user/testing --build=foo {}".format(build_arg))
+
+    assert "bar/1.0@user/testing:7839863d5a059fc6579f28026763e1021268c55e - {}".format(bar) in build_all.out
+    assert "foo/1.0@user/testing:4024617540c4f240a6a5e8911b0de9ef38a11a72 - {}".format(foo) in build_all.out
+    assert "foobar/1.0@user/testing:89636fbae346e3983af2dd63f2c5246505e74be7 - {}".format(foobar) in build_all.out
+    check_if_build_from_sources({"foo": foo, "bar": bar, "foobar": foobar}, build_all.out)
+
+
+@pytest.mark.parametrize("build_arg,bar,foo,foobar", [("--build", "Cache", "Cache", "Cache"),
+                                                      ("--build=", "Cache", "Cache", "Cache"),
+                                                      ("--build=*", "Build", "Cache", "Build")])
+def test_install_build_all_with_single_skip(build_arg, bar, foo, foobar, build_all):
+    """ When --build is passed with a skipped package, not all packages must be built from sources.
+        When --build= is passed with another package, only the package must be built from sources.
+        When --build=* is passed with another package, not all packages must be built from sources.
+
+        The arguments order matter, that's why we need to run twice.
+    """
+    for argument in ["--build=!foo {}".format(build_arg),
+                     "{} --build=!foo".format(build_arg)]:
+        build_all.run("install foobar/1.0@user/testing {}".format(argument))
+        assert "bar/1.0@user/testing:7839863d5a059fc6579f28026763e1021268c55e - {}".format(bar) in build_all.out
+        assert "foo/1.0@user/testing:4024617540c4f240a6a5e8911b0de9ef38a11a72 - {}".format(foo) in build_all.out
+        assert "foobar/1.0@user/testing:89636fbae346e3983af2dd63f2c5246505e74be7 - {}".format(foobar) in build_all.out
+        check_if_build_from_sources({"foo": foo, "bar": bar, "foobar": foobar}, build_all.out)
+
+
+@pytest.mark.parametrize("build_arg,bar,foo,foobar", [("--build", "Cache", "Cache", "Cache"),
+                                                      ("--build=", "Cache", "Cache", "Cache"),
+                                                      ("--build=*", "Cache", "Cache", "Build")])
+def test_install_build_all_with_double_skip(build_arg, bar, foo, foobar, build_all):
+    """ When --build is passed with a skipped package, not all packages must be built from sources.
+        When --build= is passed with another package, only the package must be built from sources.
+        When --build=* is passed with another package, not all packages must be built from sources.
+
+        The arguments order matter, that's why we need to run twice.
+    """
+    for argument in ["--build=!foo --build=!bar {}".format(build_arg),
+                     "{} --build=!foo --build=!bar".format(build_arg)]:
+        build_all.run("install foobar/1.0@user/testing {}".format(argument))
+
+        assert "bar/1.0@user/testing:7839863d5a059fc6579f28026763e1021268c55e - {}".format(bar) in build_all.out
+        assert "foo/1.0@user/testing:4024617540c4f240a6a5e8911b0de9ef38a11a72 - {}".format(foo) in build_all.out
+        assert "foobar/1.0@user/testing:89636fbae346e3983af2dd63f2c5246505e74be7 - {}".format(foobar) in build_all.out
+
+
+def test_report_matches(build_all):
+    """ When a wrong reference is passed to be build, an error message should be shown
+    """
+    build_all.run("install foobar/1.0@user/testing --build=* --build=baz")
+    assert "foobar/1.0@user/testing:89636fbae346e3983af2dd63f2c5246505e74be7 - Build" in build_all.out
+    assert "No package matching 'baz' pattern found." in build_all.out
+
+    build_all.run("install foobar/1.0@user/testing --build=* --build=!baz")
+    assert "No package matching 'baz' pattern found." in build_all.out
+    assert "foobar/1.0@user/testing:89636fbae346e3983af2dd63f2c5246505e74be7 - Build" in build_all.out
+
+    build_all.run("install foobar/1.0@user/testing --build=* --build=!baz --build=blah")
+    assert "No package matching 'blah' pattern found." in build_all.out
+    assert "No package matching 'baz' pattern found." in build_all.out
+    assert "foobar/1.0@user/testing:89636fbae346e3983af2dd63f2c5246505e74be7 - Build" in build_all.out
+
+    build_all.run("install foobar/1.0@user/testing --build=* --build=!baz --build=!blah")
+    assert "No package matching 'blah' pattern found." in build_all.out
+    assert "No package matching 'baz' pattern found." in build_all.out
+    assert "foobar/1.0@user/testing:89636fbae346e3983af2dd63f2c5246505e74be7 - Build" in build_all.out