Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exclude a package when building all from sources #8483

Merged
merged 19 commits into from Apr 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 3 additions & 1 deletion conans/client/command.py
Expand Up @@ -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 = []
Expand Down Expand Up @@ -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={}'.
Expand Down
29 changes: 23 additions & 6 deletions conans/client/graph/build_mode.py
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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:
Expand 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:
Expand All @@ -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)
166 changes: 166 additions & 0 deletions 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")
uilianries marked this conversation as resolved.
Show resolved Hide resolved

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")
uilianries marked this conversation as resolved.
Show resolved Hide resolved

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