Repository: conan-io/conan

Issue:
[feature] Allow defining settings for a downstream consumer (different than upstream dependencies)
Continuation from https://github.com/conan-io/conan/pull/9129

The command ``conan install . -s pkg:build_type=Debug -s build_type=Release``, to have all dependencies in Release, but build your current project as Debug has some limitations:

- It requires a conanfile.py, that defines the ``name = "pkg"``
- conanfile.txt will not allow this
- conanfile.py without a name will not allow this


The solution could be some kind of placeholder that means "current consumer", something like ``conan install . -s $:build_type=Debug -s build_type=Release``



Discussion:

Resolving patch:
diff --git a/conans/client/loader.py b/conans/client/loader.py
--- a/conans/client/loader.py
+++ b/conans/client/loader.py
@@ -186,6 +186,11 @@ def _initialize_conanfile(conanfile, profile):
             # First, try to get a match directly by name (without needing *)
             # TODO: Conan 2.0: We probably want to remove this, and leave a pure fnmatch
             pkg_settings = package_settings_values.get(conanfile.name)
+
+            if conanfile.develop and "$" in package_settings_values:
+                # "$" overrides the "name" scoped settings.
+                pkg_settings = package_settings_values.get("$")
+
             if pkg_settings is None:  # If there is not exact match by package name, do fnmatch
                 for pattern, settings in package_settings_values.items():
                     if fnmatch.fnmatchcase(ref_str, pattern):
@@ -212,10 +217,10 @@ def load_consumer(self, conanfile_path, profile_host, name=None, version=None, u
         conanfile.output.scope = conanfile.display_name
         conanfile.in_local_cache = False
         try:
+            conanfile.develop = True
             self._initialize_conanfile(conanfile, profile_host)
 
             # The consumer specific
-            conanfile.develop = True
             profile_host.user_options.descope_options(conanfile.name)
             conanfile.options.initialize_upstream(profile_host.user_options,
                                                   name=conanfile.name)
@@ -263,12 +268,18 @@ def load_conanfile_txt(self, conan_txt_path, profile_host, ref=None):
 
     def _parse_conan_txt(self, contents, path, display_name, profile):
         conanfile = ConanFile(self._output, self._runner, display_name)
+        tmp_settings = profile.processed_settings.copy()
+        package_settings_values = profile.package_settings_values
+        if "$" in package_settings_values:
+            pkg_settings = package_settings_values.get("$")
+            if pkg_settings:
+                tmp_settings.update_values(pkg_settings)
         conanfile.initialize(Settings(), profile.env_values, profile.buildenv)
         conanfile.conf = profile.conf.get_conanfile_conf(None)
         # It is necessary to copy the settings, because the above is only a constraint of
         # conanfile settings, and a txt doesn't define settings. Necessary for generators,
         # as cmake_multi, that check build_type.
-        conanfile.settings = profile.processed_settings.copy_values()
+        conanfile.settings = tmp_settings.copy_values()
 
         try:
             parser = ConanFileTextLoader(contents)


Test patch:
diff --git a/conans/test/functional/configuration/profile_test.py b/conans/test/functional/configuration/profile_test.py
--- a/conans/test/functional/configuration/profile_test.py
+++ b/conans/test/functional/configuration/profile_test.py
@@ -681,3 +681,108 @@ def test_profile_from_temp_absolute_path():
     recipe_path = os.path.join(client.current_folder, "current", "conanfile.txt")
     client.run('install "{}" -pr="{}"'.format(recipe_path, profile_path))
     assert "os=AIX" in client.out
+
+
+def test_consumer_specific_settings():
+    client = TestClient()
+    dep = str(GenConanfile().with_settings("build_type"))
+    configure = """
+    def configure(self):
+        self.output.warn("I'm {} and my build type is {}".format(self.name,
+                                                                 self.settings.build_type))
+    """
+    dep += configure
+    client.save({"conanfile.py": dep})
+    client.run("create . dep/1.0@")
+    client.run("create . dep/1.0@ -s build_type=Debug")
+
+    consumer = str(GenConanfile().with_settings("build_type").with_requires("dep/1.0"))
+    consumer += configure
+    client.save({"conanfile.py": consumer})
+
+    # Regular install with release
+    client.run("install . -s build_type=Release")
+    assert "I'm dep and my build type is Release" in client.out
+    assert "I'm None and my build type is Release" in client.out
+
+    # Now the dependency by name
+    client.run("install . -s dep:build_type=Debug")
+    assert "I'm dep and my build type is Debug" in client.out
+    assert "I'm None and my build type is Release" in client.out
+
+    # Now the consumer using $
+    client.run("install . -s $:build_type=Debug")
+    assert "I'm dep and my build type is Release" in client.out
+    assert "I'm None and my build type is Debug" in client.out
+
+    # Now use a conanfile.txt
+    client.save({"conanfile.txt": textwrap.dedent("""
+            [requires]
+            dep/1.0
+    """)}, clean_first=True)
+
+    # Regular install with release
+    client.run("install . -s build_type=Release")
+    assert "I'm dep and my build type is Release" in client.out
+
+    # Now the dependency by name
+    client.run("install . -s dep:build_type=Debug")
+    assert "I'm dep and my build type is Debug" in client.out
+
+    # Test that the generators take the setting
+    if platform.system() != "Windows":  # Toolchain in windows is multiconfig
+        # Now the consumer using $
+        client.run("install . -s $:build_type=Debug  -g CMakeToolchain")
+        assert "I'm dep and my build type is Release" in client.out
+        # Verify the cmake toolchain takes Debug
+        contents = client.load("conan_toolchain.cmake")
+        assert 'set(CMAKE_BUILD_TYPE "Debug"' in contents
+
+
+def test_create_and_priority_of_consumer_specific_setting():
+    client = TestClient()
+    conanfile = str(GenConanfile().with_settings("build_type").with_name("foo").with_version("1.0"))
+    configure = """
+    def configure(self):
+        self.output.warn("I'm {} and my build type is {}".format(self.name,
+                                                                 self.settings.build_type))
+    """
+    conanfile += configure
+    client.save({"conanfile.py": conanfile})
+    client.run("create . -s foo:build_type=Debug")
+    assert "I'm foo and my build type is Debug" in client.out
+
+    client.run("create . -s foo:build_type=Debug -s $:build_type=Release")
+    assert "I'm foo and my build type is Release" in client.out
+
+    # The order doesn't matter
+    client.run("create . -s $:build_type=Release -s foo:build_type=Debug ")
+    assert "I'm foo and my build type is Release" in client.out
+
+    # With test_package also works
+    test = str(GenConanfile().with_test("pass").with_setting("build_type"))
+    test += configure
+    client.save({"test_package/conanfile.py": test})
+    client.run("create . -s $:build_type=Debug -s build_type=Release")
+    assert "I'm foo and my build type is Debug" in client.out
+    # the test package recipe has debug too
+    assert "I'm None and my build type is Debug" in client.out
+
+
+def test_consumer_specific_settings_from_profile():
+    client = TestClient()
+    conanfile = str(GenConanfile().with_settings("build_type").with_name("hello"))
+    configure = """
+    def configure(self):
+        self.output.warn("I'm {} and my build type is {}".format(self.name,
+                                                                 self.settings.build_type))
+    """
+    conanfile += configure
+    profile = textwrap.dedent("""
+        include(default)
+        [settings]
+        $:build_type=Debug
+    """)
+    client.save({"conanfile.py": conanfile, "my_profile.txt": profile})
+    client.run("install . --profile my_profile.txt")
+    assert "I'm hello and my build type is Debug" in client.out