Repository: conan-io/conan

Issue:
'*:option=value" breaks on undefined option since 1.7.0
Here's what I get with 1.6.1 (ignore the requirement not found error, it's normal):
```
$ conan --version
Conan version 1.6.1
$ conan create ./libalmath sbre/liberforce -o gtest:shared=True -o *:shared=True -o *:fPIC=True --build=missing
libalmath/0.1@sbre/liberforce: Exporting package recipe
Linter warnings
    WARN: Linter. Line 27: FIXME: build_requires = 'qibuild'
libalmath/0.1@sbre/liberforce: The stored package has not changed
libqi/0.1@sbre/nightly: Not found in local cache, looking in remotes...
libqi/0.1@sbre/nightly: Trying with 'conan-center'...
libqi/0.1@sbre/nightly: Trying with 'sbre-public'...
ERROR: Failed requirement 'libqi/0.1@sbre/nightly' from 'libalmath/0.1@sbre/liberforce'
ERROR: Unable to find 'libqi/0.1@sbre/nightly' in remotes
```

Now here's what I get from 1.7.0 to 1.7.2:
```
$ conan --version
Conan version 1.7.0
$ conan create ./libalmath sbre/liberforce -o gtest:shared=True -o *:shared=True -o *:fPIC=True --build=missing
Exporting package recipe
Linter warnings
    WARN: Linter. Line 27: FIXME: build_requires = 'qibuild'
libalmath/0.1@sbre/liberforce: The stored package has not changed
ERROR: /home/luismenina/devel/conan/conan-public-recipes/libalmath/test_package/conanfile.py: 'options.fPIC' doesn't exist
Possible options are []
```

Has the fPIC handling changed ? Am I supposed to add it to the conanfile of my test_package?
Isn't the point of `*:option=value ` is to set the option for recipes that define the option, and let the option be ignored by others if it's not defined?

- [X] I've read the [CONTRIBUTING guide](https://raw.githubusercontent.com/conan-io/conan/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:
I've tested removing `*:fPIC=True` and I get the same for the `shared` option. This means for example that a consumer package that is an application (and so doesn't need the `shared` option), but links to one or several shared libraries can't be built using `*:shared=True` anymore. That's pretty much the use case of the test_package.

Seems related to this line in the 1.7.0 changelog:

> Bugfix: Apply general scope options to a consumer ConanFile first.

Which appears to be PR #3361 .
Yes, this could be a regression. Let us have a look, will try to fix it asap.

Resolving patch:
diff --git a/conans/client/loader.py b/conans/client/loader.py
--- a/conans/client/loader.py
+++ b/conans/client/loader.py
@@ -111,9 +111,9 @@ def load_conanfile(self, conanfile_path, output, processed_profile,
 
             if consumer:
                 conanfile.develop = True
-                processed_profile._user_options[conanfile.name].update(processed_profile._user_options["*"])
                 processed_profile._user_options.descope_options(conanfile.name)
-                conanfile.options.initialize_upstream(processed_profile._user_options, local=local)
+                conanfile.options.initialize_upstream(processed_profile._user_options, local=local,
+                                                      name=conanfile.name)
                 processed_profile._user_options.clear_unscoped_options()
 
             return conanfile
diff --git a/conans/model/options.py b/conans/model/options.py
--- a/conans/model/options.py
+++ b/conans/model/options.py
@@ -451,6 +451,12 @@ def set_local(self, values):
             except KeyError:
                 self._data.pop(k)
 
+    def initialize_patterns(self, values):
+        # Need to apply only those that exists
+        for option, value in values.items():
+            if option in self._data:
+                self._data[option].value = value
+
     def propagate_upstream(self, package_values, down_ref, own_ref, pattern_options):
         """
         :param: package_values: PackageOptionValues({"shared": "True"}
@@ -570,7 +576,7 @@ def propagate_upstream(self, down_package_values, down_ref, own_ref):
                 pkg_values = self._deps_package_values.setdefault(name, PackageOptionValues())
                 pkg_values.propagate_upstream(option_values, down_ref, own_ref, name)
 
-    def initialize_upstream(self, user_values, local=False):
+    def initialize_upstream(self, user_values, local=False, name=None):
         """ used to propagate from downstream the options to the upper requirements
         """
         if user_values is not None:
@@ -579,6 +585,12 @@ def initialize_upstream(self, user_values, local=False):
             if local:
                 self._package_options.set_local(user_values._package_values)
             else:
+                # This code is necessary to process patterns like *:shared=True
+                # To apply to the current consumer, which might not have name
+                for pattern, pkg_options in sorted(user_values._reqs_options.items()):
+                    if fnmatch.fnmatch(name or "", pattern):
+                        self._package_options.initialize_patterns(pkg_options)
+                # Then, the normal assignment of values, which could override patterns
                 self._package_options.values = user_values._package_values
             for package_name, package_values in user_values._reqs_options.items():
                 pkg_values = self._deps_package_values.setdefault(package_name, PackageOptionValues())


Test patch:
diff --git a/conans/test/integration/options_test.py b/conans/test/integration/options_test.py
--- a/conans/test/integration/options_test.py
+++ b/conans/test/integration/options_test.py
@@ -7,6 +7,83 @@
 
 class OptionsTest(unittest.TestCase):
 
+    def general_scope_options_test_package_test(self):
+        client = TestClient()
+        conanfile = """from conans import ConanFile
+class Pkg(ConanFile):
+    options = {"shared": ["1", "2"]}
+    def configure(self):
+        self.output.info("BUILD SHARED: %s" % self.options.shared)
+"""
+        test = """from conans import ConanFile
+class Pkg(ConanFile):
+    def test(self):
+        pass
+"""
+        client.save({"conanfile.py": conanfile})
+        client.run("create . Pkg/0.1@user/testing -o *:shared=1")
+        self.assertIn("Pkg/0.1@user/testing: BUILD SHARED: 1", client.out)
+        client.run("create . Pkg/0.1@user/testing -o shared=2")
+        self.assertIn("Pkg/0.1@user/testing: BUILD SHARED: 2", client.out)
+        # With test_package
+        client.save({"conanfile.py": conanfile,
+                     "test_package/conanfile.py": test})
+        client.run("create . Pkg/0.1@user/testing -o *:shared=1")
+        self.assertIn("Pkg/0.1@user/testing: BUILD SHARED: 1", client.out)
+        client.run("create . Pkg/0.1@user/testing -o Pkg:shared=2")
+        self.assertIn("Pkg/0.1@user/testing: BUILD SHARED: 2", client.out)
+        error = client.run("create . Pkg/0.1@user/testing -o shared=1", ignore_error=True)
+        self.assertTrue(error)
+        self.assertIn("'options.shared' doesn't exist", client.out)
+
+        conanfile = """from conans import ConanFile
+class Pkg(ConanFile):
+    pass
+"""
+        client.save({"conanfile.py": conanfile}, clean_first=True)
+        client.run("create . Pkg/0.1@user/testing -o *:shared=True")
+        self.assertIn("Pkg/0.1@user/testing: Calling build()", client.out)
+        error = client.run("create . Pkg/0.1@user/testing -o shared=False", ignore_error=True)
+        self.assertTrue(error)
+        self.assertIn("'options.shared' doesn't exist", client.out)
+        # With test_package
+        client.save({"conanfile.py": conanfile,
+                     "test_package/conanfile.py": test})
+        client.run("create . Pkg/0.1@user/testing -o *:shared=True")
+        self.assertIn("Pkg/0.1@user/testing: Calling build()", client.out)
+        self.assertIn("Pkg/0.1@user/testing (test package): Running build()", client.out)
+
+    def general_scope_priorities_test(self):
+        client = TestClient()
+        conanfile = """from conans import ConanFile
+class Pkg(ConanFile):
+    options = {"shared": ["1", "2", "3"]}
+    def configure(self):
+        self.output.info("BUILD SHARED: %s" % self.options.shared)
+"""
+        test = """from conans import ConanFile
+class Pkg(ConanFile):
+    def test(self):
+        pass
+"""
+        client.save({"conanfile.py": conanfile})
+        # Consumer has priority
+        client.run("create . Pkg/0.1@user/testing -o *:shared=1 -o shared=2")
+        self.assertIn("Pkg/0.1@user/testing: BUILD SHARED: 2", client.out)
+        # Consumer has priority over pattern, even if the pattern specifies the package name
+        client.run("create . Pkg/0.1@user/testing -o *:shared=1 -o Pkg:shared=2 -o shared=3")
+        self.assertIn("Pkg/0.1@user/testing: BUILD SHARED: 3", client.out)
+        # With test_package
+        client.save({"conanfile.py": conanfile,
+                     "test_package/conanfile.py": test})
+        # Sorted (longest, alphabetical) patterns, have priority
+        client.run("create . Pkg/0.1@user/testing -o *:shared=1 -o Pkg:shared=2")
+        self.assertIn("Pkg/0.1@user/testing: BUILD SHARED: 2", client.out)
+        client.run("create . Pkg/0.1@user/testing  -o Pk*:shared=2 -o P*:shared=1")
+        self.assertIn("Pkg/0.1@user/testing: BUILD SHARED: 2", client.out)
+        client.run("create . Pkg/0.1@user/testing  -o Pk*:shared=2 -o P*:shared=1")
+        self.assertIn("Pkg/0.1@user/testing: BUILD SHARED: 2", client.out)
+
     def parsing_test(self):
         client = TestClient()
         conanfile = '''
@@ -68,55 +145,43 @@ class ConanLib(ConanFile):
     def general_scope_options_test(self):
         # https://github.com/conan-io/conan/issues/2538
         client = TestClient()
-        conanfile_libA = """
-from conans import ConanFile
-
+        conanfile_libA = """from conans import ConanFile
 class LibA(ConanFile):
-    name = "libA"
-    version = "0.1"
     options = {"shared": [True, False]}
-    default_options= "shared=False"
 
     def configure(self):
         self.output.info("shared=%s" % self.options.shared)
     """
-        conanfile_libB = """
-from conans import ConanFile
+        client.save({"conanfile.py": conanfile_libA})
+        client.run("create . libA/0.1@danimtb/testing -o *:shared=True")
+        self.assertIn("libA/0.1@danimtb/testing: shared=True", client.out)
 
+        conanfile_libB = """from conans import ConanFile
 class LibB(ConanFile):
-    name = "libB"
-    version = "0.1"
     options = {"shared": [True, False]}
-    default_options= "shared=False"
     requires = "libA/0.1@danimtb/testing"
 
     def configure(self):
         self.options["*"].shared = self.options.shared
         self.output.info("shared=%s" % self.options.shared)
     """
-        client.save({"conanfile_liba.py": conanfile_libA,
-                     "conanfile_libb.py": conanfile_libB})
 
         for without_configure_line in [True, False]:
-            client.save({"conanfile_liba.py": conanfile_libA})
-
             if without_configure_line:
-                client.save({"conanfile_libb.py": conanfile_libB.replace(
-                    "        self.options[\"*\"].shared = self.options.shared", "")})
-            else:
-                client.save({"conanfile_libb.py": conanfile_libB})
+                conanfile = conanfile_libB.replace(
+                    "        self.options[\"*\"].shared = self.options.shared", "")
+
+            client.save({"conanfile.py": conanfile})
 
             # Test info
-            client.run("export conanfile_liba.py danimtb/testing")
-            client.run("info conanfile_libb.py -o *:shared=True")
+            client.run("info . -o *:shared=True", ignore_error=True)
             self.assertIn("PROJECT: shared=True", client.out)
             self.assertIn("libA/0.1@danimtb/testing: shared=True", client.out)
             # Test create
-            client.run("create conanfile_liba.py danimtb/testing -o *:shared=True")
-            client.run("create conanfile_libb.py danimtb/testing -o *:shared=True")
+            client.run("create . libB/0.1@danimtb/testing -o *:shared=True")
             self.assertIn("libB/0.1@danimtb/testing: shared=True", client.out)
             self.assertIn("libA/0.1@danimtb/testing: shared=True", client.out)
             # Test install
-            client.run("install conanfile_libb.py -o *:shared=True")
+            client.run("install . -o *:shared=True")
             self.assertIn("PROJECT: shared=True", client.out)
             self.assertIn("libA/0.1@danimtb/testing: shared=True", client.out)