Repository: conan-io/conan
Pull request: Feature/validate
Changelog: Feature: Introduce a new `BINARY_INVALID` mode for more flexible definition and management of invalid configurations.
Docs: https://github.com/conan-io/docs/pull/1947

Close https://github.com/conan-io/conan/issues/7591

This PR satisfies 2 current proposals/requests:

- The need to specify "impossible" binaries after the graph is fully compute, options are propagated down to consumers, real requirements are also updated (not declared ones, but the actual computed ones, there can be overrides). Feature request in https://github.com/conan-io/conan/issues/7591

- The need to specify that something cannot be built, but it is possible to have a compatible package defined in ``package_id()``, ``compatible_packages``. PR https://github.com/conan-io/conan/pull/6952

The current proposal provides a relatively straightforward solution for both by adding a new explicit model of BINARY_INVALID, in the same way we already have a BINARY_UNKNOWN. This model will also be good for future installation f ``build_requires`` that do not exist in a host system, because they only exist in the build context, without needing to explicitly define the build profile.

It also supports asking for "what/if need to build" with ``conan info``, it can explicitly return ID: INVALID without raising an exception, so it can be easily used in ConanCenter for example.

There are still some corner cases to be managed (like the processing after a BINARY_UNKNOWN, if that case can happen), or how BINARY_INVALID can produce a BINARY_UNKNOWN if required downstream, but I prefered to propose the core concept before completing it.


Problem statement:
[feature] Method to be run after the graph is resolved
There are some checks that are needed and cannot be run in the `configure()` method:
 * values from options of the requirements
 * actual version of the requirements (if using version-ranges or overriding).

**Motivation**

Right now, these checks are moved to the `build()` method, which is the only (the first) method were all these values are available. Recipe can check for options values and it can use `deps_cpp_info["xxx"].version` to check the actual version of the requirements.

The big issue is that this method is executed only if the package is going to be built, but if the package is already available we won't notice the error until it is too late (runtime error). Depending on the `package_id_mode`, the consumer will get a different package-id **or not** for the given scenarios above. So in the general use-case, this new method is something needed.

**Proposal**

Let's add a new method to run these checks:

```python

class Recipe(ConanFile):
   def validate(self):
      if self.options["req1"].shared == True:
          raise ConanInvalidConfiguration("req1 needs to be statically linked")

      if self.<requirements>["req1"].version >= 7.0:
         raise ConanInvalidConfiguration(f"{self.name} requires 'req1<7.0'") 
```

This method will run after the graph is built for the same Conan commands `configure()` is running right now.

Hints from issue discussion:
For hooks I think we can use `pre_build`, but for regular validation this new feature looks interesting. 
It would be nice if conan was able to report a summary of options/requirements violations, instead of just raise at the first error. Otherwise, consumers may fall in a "die and retry" or "read your 100 conanfiles" to figure out what to do to solve those issues.
> It would be nice if conan was able to report a summary of options/requirements violations, instead of just raise at the first error. Otherwise, consumers may fall in a "die and retry" or "read your 100 conanfiles" to figure out what to do to solve those issues.

Yes, this is something to take into account for the graph model in Conan 2.0, but I want to limit the expectations a little bit: requirements are conditional to options and settings, so as soon as we find a conflict we cannot go ahead in that branch of the graph (it might not be the right branch once the conflict is solved), but I agree we can inspect other branches and report errors that are not affected by existing ones. 👍

Commit message: add conan info --json checks

Patch to review:
diff --git a/conans/client/graph/graph.py b/conans/client/graph/graph.py
index 8c842c974c9..c49bb8e3851 100644
--- a/conans/client/graph/graph.py
+++ b/conans/client/graph/graph.py
@@ -21,6 +21,7 @@
 BINARY_SKIP = "Skip"
 BINARY_EDITABLE = "Editable"
 BINARY_UNKNOWN = "Unknown"
+BINARY_INVALID = "Invalid"
 
 CONTEXT_HOST = "host"
 CONTEXT_BUILD = "build"
diff --git a/conans/client/graph/graph_binaries.py b/conans/client/graph/graph_binaries.py
index 6b5e5c2fd8f..b9b5d7290f6 100644
--- a/conans/client/graph/graph_binaries.py
+++ b/conans/client/graph/graph_binaries.py
@@ -1,10 +1,11 @@
 from conans.client.graph.build_mode import BuildMode
 from conans.client.graph.graph import (BINARY_BUILD, BINARY_CACHE, BINARY_DOWNLOAD, BINARY_MISSING,
                                        BINARY_UPDATE, RECIPE_EDITABLE, BINARY_EDITABLE,
-                                       RECIPE_CONSUMER, RECIPE_VIRTUAL, BINARY_SKIP, BINARY_UNKNOWN)
+                                       RECIPE_CONSUMER, RECIPE_VIRTUAL, BINARY_SKIP, BINARY_UNKNOWN,
+                                       BINARY_INVALID)
 from conans.errors import NoRemoteAvailable, NotFoundException, conanfile_exception_formatter, \
-    ConanException
-from conans.model.info import ConanInfo, PACKAGE_ID_UNKNOWN
+    ConanException, ConanInvalidConfiguration
+from conans.model.info import ConanInfo, PACKAGE_ID_UNKNOWN, PACKAGE_ID_INVALID
 from conans.model.manifest import FileTreeManifest
 from conans.model.ref import PackageReference
 from conans.util.conan_v2_mode import conan_v2_property
@@ -173,7 +174,7 @@ def _evaluate_node(self, node, build_mode, update, remotes):
             assert node.binary is None, "Node.binary should be None if not locked"
             pref = PackageReference(node.ref, node.package_id)
             self._process_node(node, pref, build_mode, update, remotes)
-            if node.binary == BINARY_MISSING:
+            if node.binary in (BINARY_MISSING, BINARY_INVALID):
                 if node.conanfile.compatible_packages:
                     compatible_build_mode = BuildMode(None, self._out)
                     for compatible_package in node.conanfile.compatible_packages:
@@ -187,16 +188,19 @@ def _evaluate_node(self, node, build_mode, update, remotes):
                         # NO Build mode
                         self._process_node(node, pref, compatible_build_mode, update, remotes)
                         assert node.binary is not None
-                        if node.binary != BINARY_MISSING:
+                        if node.binary not in BINARY_MISSING:
                             node.conanfile.output.info("Main binary package '%s' missing. Using "
                                                        "compatible package '%s'"
                                                        % (node.package_id, package_id))
+
                             # Modifying package id under the hood, FIXME
                             node._package_id = package_id
                             # So they are available in package_info() method
                             node.conanfile.settings.values = compatible_package.settings
                             node.conanfile.options.values = compatible_package.options
                             break
+                    if node.binary == BINARY_MISSING and node.package_id == PACKAGE_ID_INVALID:
+                        node.binary = BINARY_INVALID
                 if node.binary == BINARY_MISSING and build_mode.allowed(node.conanfile):
                     node.binary = BINARY_BUILD
 
@@ -214,6 +218,12 @@ def _process_node(self, node, pref, build_mode, update, remotes):
             node.binary = BINARY_EDITABLE  # TODO: PREV?
             return
 
+        if pref.id == PACKAGE_ID_INVALID:
+            # annotate pattern, so unused patterns in --build are not displayed as errors
+            build_mode.forced(node.conanfile, node.ref)
+            node.binary = BINARY_INVALID
+            return
+
         if self._evaluate_build(node, build_mode):
             return
 
@@ -336,9 +346,17 @@ def _compute_package_id(self, node, default_package_id_mode, default_python_requ
                                    "'self.cpp_info' access in package_id() method is deprecated"):
                 conanfile.package_id()
 
+        if hasattr(conanfile, "validate") and callable(conanfile.validate):
+            with conanfile_exception_formatter(str(conanfile), "validate"):
+                try:
+                    conanfile.validate()
+                except ConanInvalidConfiguration as e:
+                    conanfile.info.invalid = str(e)
+
         info = conanfile.info
         node.package_id = info.package_id()
 
+
     def evaluate_graph(self, deps_graph, build_mode, update, remotes, nodes_subset=None, root=None):
         default_package_id_mode = self._cache.config.default_package_id_mode
         default_python_requires_id_mode = self._cache.config.default_python_requires_id_mode
@@ -346,6 +364,7 @@ def evaluate_graph(self, deps_graph, build_mode, update, remotes, nodes_subset=N
             self._propagate_options(node)
 
             self._compute_package_id(node, default_package_id_mode, default_python_requires_id_mode)
+
             if node.recipe in (RECIPE_CONSUMER, RECIPE_VIRTUAL):
                 continue
             if node.package_id == PACKAGE_ID_UNKNOWN:
@@ -368,6 +387,7 @@ def reevaluate_node(self, node, remotes, build_mode, update):
         default_python_requires_id_mode = self._cache.config.default_python_requires_id_mode
         output.info("Unknown binary for %s, computing updated ID" % str(node.ref))
         self._compute_package_id(node, default_package_id_mode, default_python_requires_id_mode)
+
         output.info("Updated ID: %s" % node.package_id)
         if node.recipe in (RECIPE_CONSUMER, RECIPE_VIRTUAL):
             return
diff --git a/conans/client/installer.py b/conans/client/installer.py
index d1f20b55a9a..0d8118b6da2 100644
--- a/conans/client/installer.py
+++ b/conans/client/installer.py
@@ -10,7 +10,7 @@
 from conans.client.file_copier import report_copied_files
 from conans.client.generators import TXTGenerator
 from conans.client.graph.graph import BINARY_BUILD, BINARY_CACHE, BINARY_DOWNLOAD, BINARY_EDITABLE, \
-    BINARY_MISSING, BINARY_SKIP, BINARY_UPDATE, BINARY_UNKNOWN, CONTEXT_HOST
+    BINARY_MISSING, BINARY_SKIP, BINARY_UPDATE, BINARY_UNKNOWN, CONTEXT_HOST, BINARY_INVALID
 from conans.client.importer import remove_imports, run_imports
 from conans.client.packager import update_package_metadata
 from conans.client.recorder.action_recorder import INSTALL_ERROR_BUILDING, INSTALL_ERROR_MISSING, \
@@ -20,7 +20,7 @@
 from conans.client.tools.env import no_op
 from conans.client.tools.env import pythonpath
 from conans.errors import (ConanException, ConanExceptionInUserConanfileMethod,
-                           conanfile_exception_formatter)
+                           conanfile_exception_formatter, ConanInvalidConfiguration)
 from conans.model.build_info import CppInfo, DepCppInfo
 from conans.model.conan_file import ConanFile
 from conans.model.editable_layout import EditableLayout
@@ -307,14 +307,16 @@ def install(self, deps_graph, remotes, build_mode, update, keep_build=False, gra
 
     @staticmethod
     def _classify(nodes_by_level):
-        missing, downloads = [], []
+        missing, invalid, downloads = [], [], []
         for level in nodes_by_level:
             for node in level:
                 if node.binary == BINARY_MISSING:
                     missing.append(node)
+                elif node.binary == BINARY_INVALID:
+                    invalid.append(node)
                 elif node.binary in (BINARY_UPDATE, BINARY_DOWNLOAD):
                     downloads.append(node)
-        return missing, downloads
+        return missing, invalid, downloads
 
     def _raise_missing(self, missing):
         if not missing:
@@ -403,7 +405,11 @@ def _download_pkg(self, layout, node):
 
     def _build(self, nodes_by_level, keep_build, root_node, graph_info, remotes, build_mode, update):
         using_build_profile = bool(graph_info.profile_build)
-        missing, downloads = self._classify(nodes_by_level)
+        missing, invalid, downloads = self._classify(nodes_by_level)
+        if invalid:
+            node = invalid[0]  # Raise the first one
+            msg = "{}: Invalid ID: {}".format(node.conanfile, node.conanfile.info.invalid)
+            raise ConanInvalidConfiguration(msg)
         self._raise_missing(missing)
         processed_package_refs = set()
         self._download(downloads, processed_package_refs)
@@ -427,6 +433,7 @@ def _build(self, nodes_by_level, keep_build, root_node, graph_info, remotes, bui
                         self._binaries_analyzer.reevaluate_node(node, remotes, build_mode, update)
                         if node.binary == BINARY_MISSING:
                             self._raise_missing([node])
+                        # TODO: Check if BINARY_INVALID?
                     _handle_system_requirements(conan_file, node.pref, self._cache, output)
                     self._handle_node_cache(node, keep_build, processed_package_refs, remotes)
 
diff --git a/conans/model/info.py b/conans/model/info.py
index e1e0b6a2a6a..fc0cd4ec376 100644
--- a/conans/model/info.py
+++ b/conans/model/info.py
@@ -14,6 +14,7 @@
 
 PREV_UNKNOWN = "PREV unknown"
 PACKAGE_ID_UNKNOWN = "Package_ID_unknown"
+PACKAGE_ID_INVALID = "INVALID"
 
 
 class RequirementInfo(object):
@@ -425,6 +426,7 @@ def copy(self):
         """ Useful for build_id implementation
         """
         result = ConanInfo()
+        result.invalid = self.invalid
         result.settings = self.settings.copy()
         result.options = self.options.copy()
         result.requires = self.requires.copy()
@@ -435,6 +437,7 @@ def copy(self):
     def create(settings, options, prefs_direct, prefs_indirect, default_package_id_mode,
                python_requires, default_python_requires_id_mode):
         result = ConanInfo()
+        result.invalid = None
         result.full_settings = settings
         result.settings = settings.copy()
         result.full_options = options
@@ -461,6 +464,7 @@ def loads(text):
                                      "requires", "full_requires", "scope", "recipe_hash", "env"],
                               raise_unexpected_field=False)
         result = ConanInfo()
+        result.invalid = None
         result.settings = Values.loads(parser.settings)
         result.full_settings = Values.loads(parser.full_settings)
         result.options = OptionsValues.loads(parser.options)
@@ -534,6 +538,8 @@ def package_id(self):
         """ The package_id of a conans is the sha1 of its specific requirements,
         options and settings
         """
+        if self.invalid:
+            return PACKAGE_ID_INVALID
         result = [self.settings.sha]
         # Only are valid requires for OPtions those Non-Dev who are still in requires
         self.options.filter_used(self.requires.pkg_names)
diff --git a/conans/test/functional/package_id/test_validate.py b/conans/test/functional/package_id/test_validate.py
new file mode 100644
index 00000000000..e3f96474fd0
--- /dev/null
+++ b/conans/test/functional/package_id/test_validate.py
@@ -0,0 +1,224 @@
+import json
+import textwrap
+import unittest
+
+from conans.cli.exit_codes import ERROR_INVALID_CONFIGURATION
+from conans.client.graph.graph import BINARY_INVALID
+from conans.model.info import PACKAGE_ID_INVALID
+from conans.test.assets.genconanfile import GenConanfile
+from conans.test.utils.tools import TestClient
+
+
+class TestValidate(unittest.TestCase):
+
+    def test_validate_create(self):
+        client = TestClient()
+        conanfile = textwrap.dedent("""
+            from conans import ConanFile
+            from conans.errors import ConanInvalidConfiguration
+            class Pkg(ConanFile):
+                settings = "os"
+
+                def validate(self):
+                    if self.settings.os == "Windows":
+                        raise ConanInvalidConfiguration("Windows not supported")
+            """)
+
+        client.save({"conanfile.py": conanfile})
+
+        client.run("create . pkg/0.1@ -s os=Linux")
+        self.assertIn("pkg/0.1: Package 'cb054d0b3e1ca595dc66bc2339d40f1f8f04ab31' created",
+                      client.out)
+
+        error = client.run("create . pkg/0.1@ -s os=Windows", assert_error=True)
+        self.assertEqual(error, ERROR_INVALID_CONFIGURATION)
+        self.assertIn("ERROR: pkg/0.1: Invalid ID: Windows not supported", client.out)
+        client.run("info pkg/0.1@ -s os=Windows")
+        self.assertIn("ID: INVALID", client.out)
+        client.run("info pkg/0.1@ -s os=Windows --json=myjson")
+        myjson = json.loads(client.load("myjson"))
+        self.assertEqual(myjson[0]["binary"], BINARY_INVALID)
+        self.assertEqual(myjson[0]["id"], PACKAGE_ID_INVALID)
+
+    def test_validate_compatible(self):
+        client = TestClient()
+        conanfile = textwrap.dedent("""
+            from conans import ConanFile
+            from conans.errors import ConanInvalidConfiguration
+            class Pkg(ConanFile):
+                settings = "os"
+
+                def validate(self):
+                    if self.settings.os == "Windows":
+                        raise ConanInvalidConfiguration("Windows not supported")
+
+                def package_id(self):
+                    if self.settings.os == "Windows":
+                        compatible_pkg = self.info.clone()
+                        compatible_pkg.settings.os = "Linux"
+                        self.compatible_packages.append(compatible_pkg)
+            """)
+
+        client.save({"conanfile.py": conanfile})
+
+        client.run("create . pkg/0.1@ -s os=Linux")
+        self.assertIn("pkg/0.1: Package 'cb054d0b3e1ca595dc66bc2339d40f1f8f04ab31' created",
+                      client.out)
+
+        client.run("create . pkg/0.1@ -s os=Windows")
+        self.assertIn("pkg/0.1: Main binary package 'INVALID' missing. "
+                      "Using compatible package 'cb054d0b3e1ca595dc66bc2339d40f1f8f04ab31'",
+                      client.out)
+        self.assertIn("pkg/0.1:cb054d0b3e1ca595dc66bc2339d40f1f8f04ab31 - Cache", client.out)
+        client.run("info pkg/0.1@ -s os=Windows")
+        self.assertIn("pkg/0.1: Main binary package 'INVALID' missing. "
+                      "Using compatible package 'cb054d0b3e1ca595dc66bc2339d40f1f8f04ab31'",
+                      client.out)
+        self.assertIn("ID: cb054d0b3e1ca595dc66bc2339d40f1f8f04ab31", client.out)
+
+    def test_validate_compatible_also_invalid(self):
+        client = TestClient()
+        conanfile = textwrap.dedent("""
+           from conans import ConanFile
+           from conans.errors import ConanInvalidConfiguration
+           class Pkg(ConanFile):
+               settings = "os", "build_type"
+
+               def validate(self):
+                   if self.settings.os == "Windows":
+                       raise ConanInvalidConfiguration("Windows not supported")
+
+               def package_id(self):
+                   if self.settings.build_type == "Debug" and self.settings.os != "Windows":
+                       compatible_pkg = self.info.clone()
+                       compatible_pkg.settings.build_type = "Release"
+                       self.compatible_packages.append(compatible_pkg)
+               """)
+
+        client.save({"conanfile.py": conanfile})
+
+        client.run("create . pkg/0.1@ -s os=Linux -s build_type=Release")
+        self.assertIn("pkg/0.1: Package '24c3aa2d6c5929d53bd86b31e020c55d96b265c7' created",
+                      client.out)
+        # compatible_packges fallback works
+        client.run("install pkg/0.1@ -s os=Linux -s build_type=Debug")
+        self.assertIn("pkg/0.1:24c3aa2d6c5929d53bd86b31e020c55d96b265c7 - Cache", client.out)
+
+        error = client.run("create . pkg/0.1@ -s os=Windows -s build_type=Release",
+                           assert_error=True)
+        self.assertEqual(error, ERROR_INVALID_CONFIGURATION)
+        self.assertIn("ERROR: pkg/0.1: Invalid ID: Windows not supported", client.out)
+
+        client.run("info pkg/0.1@ -s os=Windows")
+        self.assertIn("ID: INVALID", client.out)
+
+    def test_validate_compatible_also_invalid_fail(self):
+        client = TestClient()
+        conanfile = textwrap.dedent("""
+           from conans import ConanFile
+           from conans.errors import ConanInvalidConfiguration
+           class Pkg(ConanFile):
+               settings = "os", "build_type"
+
+               def validate(self):
+                   if self.settings.os == "Windows":
+                       raise ConanInvalidConfiguration("Windows not supported")
+
+               def package_id(self):
+                   if self.settings.build_type == "Debug":
+                       compatible_pkg = self.info.clone()
+                       compatible_pkg.settings.build_type = "Release"
+                       self.compatible_packages.append(compatible_pkg)
+               """)
+
+        client.save({"conanfile.py": conanfile})
+
+        client.run("create . pkg/0.1@ -s os=Linux -s build_type=Release")
+        self.assertIn("pkg/0.1: Package '24c3aa2d6c5929d53bd86b31e020c55d96b265c7' created",
+                      client.out)
+        # compatible_packges fallback works
+        client.run("install pkg/0.1@ -s os=Linux -s build_type=Debug")
+        self.assertIn("pkg/0.1:24c3aa2d6c5929d53bd86b31e020c55d96b265c7 - Cache", client.out)
+
+        # Windows invalid configuration
+        error = client.run("create . pkg/0.1@ -s os=Windows -s build_type=Release",
+                           assert_error=True)
+        self.assertEqual(error, ERROR_INVALID_CONFIGURATION)
+        self.assertIn("ERROR: pkg/0.1: Invalid ID: Windows not supported", client.out)
+
+        error = client.run("install pkg/0.1@ -s os=Windows -s build_type=Release",
+                           assert_error=True)
+        self.assertEqual(error, ERROR_INVALID_CONFIGURATION)
+        self.assertIn("ERROR: pkg/0.1: Invalid ID: Windows not supported", client.out)
+
+        # Windows missing binary: INVALID
+        error = client.run("install pkg/0.1@ -s os=Windows -s build_type=Debug",
+                           assert_error=True)
+        self.assertEqual(error, ERROR_INVALID_CONFIGURATION)
+        self.assertIn("ERROR: pkg/0.1: Invalid ID: Windows not supported", client.out)
+
+        error = client.run("create . pkg/0.1@ -s os=Windows -s build_type=Debug",
+                           assert_error=True)
+        self.assertEqual(error, ERROR_INVALID_CONFIGURATION)
+        self.assertIn("ERROR: pkg/0.1: Invalid ID: Windows not supported", client.out)
+
+        # info
+        client.run("info pkg/0.1@ -s os=Windows")
+        self.assertIn("ID: INVALID", client.out)
+        client.run("info pkg/0.1@ -s os=Windows -s build_type=Debug")
+        self.assertIn("ID: INVALID", client.out)
+
+    def test_validate_options(self):
+        client = TestClient()
+        client.save({"conanfile.py": GenConanfile().with_option("myoption", [1, 2, 3])
+                                                   .with_default_option("myoption", 1)})
+        client.run("create . dep/0.1@")
+        client.run("create . dep/0.1@ -o dep:myoption=2")
+        conanfile = textwrap.dedent("""
+           from conans import ConanFile
+           from conans.errors import ConanInvalidConfiguration
+           class Pkg(ConanFile):
+               requires = "dep/0.1"
+
+               def validate(self):
+                   if self.options["dep"].myoption == 2:
+                       raise ConanInvalidConfiguration("Option 2 of 'dep' not supported")
+           """)
+
+        client.save({"conanfile.py": conanfile})
+        client.run("create . pkg1/0.1@ -o dep:myoption=1")
+
+        client.save({"conanfile.py": GenConanfile().with_requires("dep/0.1")
+                                                   .with_default_option("dep:myoption", 2)})
+        client.run("create . pkg2/0.1@")
+
+        client.save({"conanfile.py": GenConanfile().with_requires("pkg1/0.1", "pkg2/0.1")})
+        error = client.run("install .", assert_error=True)
+        self.assertEqual(error, ERROR_INVALID_CONFIGURATION)
+        self.assertIn("ERROR: pkg1/0.1: Invalid ID: Option 2 of 'dep' not supported", client.out)
+
+    def test_validate_requires(self):
+        client = TestClient()
+        client.save({"conanfile.py": GenConanfile()})
+        client.run("create . dep/0.1@")
+        client.run("create . dep/0.2@")
+        conanfile = textwrap.dedent("""
+           from conans import ConanFile
+           from conans.errors import ConanInvalidConfiguration
+           class Pkg(ConanFile):
+               requires = "dep/0.1"
+
+               def validate(self):
+                   # FIXME: This is a ugly interface
+                   # if self.info.requires["dep"].full_version ==
+                   if self.requires["dep"].ref.version > "0.1":
+                       raise ConanInvalidConfiguration("dep> 0.1 is not supported")
+           """)
+
+        client.save({"conanfile.py": conanfile})
+        client.run("create . pkg1/0.1@")
+
+        client.save({"conanfile.py": GenConanfile().with_requires("pkg1/0.1", "dep/0.2")})
+        error = client.run("install .", assert_error=True)
+        self.assertEqual(error, ERROR_INVALID_CONFIGURATION)
+        self.assertIn("ERROR: pkg1/0.1: Invalid ID: dep> 0.1 is not supported", client.out)


Code review comments:
diff_hunk: @@ -0,0 +1,224 @@
+import json
+import textwrap
+import unittest
+
+from conans.cli.exit_codes import ERROR_INVALID_CONFIGURATION
+from conans.client.graph.graph import BINARY_INVALID
+from conans.model.info import PACKAGE_ID_INVALID
+from conans.test.assets.genconanfile import GenConanfile
+from conans.test.utils.tools import TestClient
+
+
+class TestValidate(unittest.TestCase):
+
+    def test_validate_create(self):
+        client = TestClient()
+        conanfile = textwrap.dedent("""
+            from conans import ConanFile
+            from conans.errors import ConanInvalidConfiguration
+            class Pkg(ConanFile):
+                settings = "os"
+
+                def validate(self):
+                    if self.settings.os == "Windows":
+                        raise ConanInvalidConfiguration("Windows not supported")
+            """)
+
+        client.save({"conanfile.py": conanfile})
+
+        client.run("create . pkg/0.1@ -s os=Linux")
+        self.assertIn("pkg/0.1: Package 'cb054d0b3e1ca595dc66bc2339d40f1f8f04ab31' created",
+                      client.out)
+
+        error = client.run("create . pkg/0.1@ -s os=Windows", assert_error=True)
+        self.assertEqual(error, ERROR_INVALID_CONFIGURATION)
+        self.assertIn("ERROR: pkg/0.1: Invalid ID: Windows not supported", client.out)
+        client.run("info pkg/0.1@ -s os=Windows")
+        self.assertIn("ID: INVALID", client.out)
+        client.run("info pkg/0.1@ -s os=Windows --json=myjson")
+        myjson = json.loads(client.load("myjson"))
+        self.assertEqual(myjson[0]["binary"], BINARY_INVALID)
+        self.assertEqual(myjson[0]["id"], PACKAGE_ID_INVALID)
path: conans/test/functional/package_id/test_validate.py
text: @user1:
```suggestion
        self.assertEqual(myjson[0]["id"], 'Invalid')
```