Repository: conan-io/conan
Pull request #18245 (2025-05-06 14:37:12)

Fix edge 0 information for conan graph info ... -f=html 
Changelog: Bugfix: The first edge on `conan graph info ... -f=html` now shows require information
Docs: Omit
Close: https://github.com/conan-io/conan/issues/18239



Patch:
diff --git a/conan/cli/formatters/graph/info_graph_html.py b/conan/cli/formatters/graph/info_graph_html.py
index 60a31c28f..da0a4964c 100644
--- a/conan/cli/formatters/graph/info_graph_html.py
+++ b/conan/cli/formatters/graph/info_graph_html.py
@@ -292,7 +292,7 @@ graph_info_html = r"""
                 while (control.firstChild) {
                     control.removeChild(control.firstChild);
                 }
-                if(ids[0] || ids_edges[0]) {
+                if(ids[0] !== undefined || ids_edges[0] !== undefined) {
                     selected = graph_data["nodes"][ids[0]] || global_edges[ids_edges[0]];
                     let div = document.createElement('div');
                     let f = Object.fromEntries(Object.entries(selected).filter(([_, v]) => v != null));
diff --git a/conan/internal/api/profile/profile_loader.py b/conan/internal/api/profile/profile_loader.py
index b0f67af1d..b9265b8c2 100644
--- a/conan/internal/api/profile/profile_loader.py
+++ b/conan/internal/api/profile/profile_loader.py
@@ -1,5 +1,6 @@
 import os
 import platform
+import subprocess
 from collections import OrderedDict, defaultdict
 
 from jinja2 import Environment, FileSystemLoader
@@ -118,6 +119,7 @@ class ProfileLoader:
         file_path = os.path.basename(profile_path)
         context = {"platform": platform,
                    "os": os,
+                   "subprocess": subprocess,
                    "profile_dir": base_path,
                    "profile_name": file_path,
                    "conan_version": conan_version,
diff --git a/conan/tools/gnu/pkgconfigdeps.py b/conan/tools/gnu/pkgconfigdeps.py
index f10501fa2..4c95e1426 100644
--- a/conan/tools/gnu/pkgconfigdeps.py
+++ b/conan/tools/gnu/pkgconfigdeps.py
@@ -1,31 +1,16 @@
 import os
 import re
 import textwrap
-from collections import namedtuple
 
 from jinja2 import Template, StrictUndefined
 
 from conan.errors import ConanException
 from conan.internal import check_duplicated_generator
-from conan.tools.gnu.gnudeps_flags import GnuDepsFlags
 from conan.internal.model.dependencies import get_transitive_requires
 from conan.internal.util.files import save
 
 
-_PCInfo = namedtuple("PCInfo", ['name', 'version', 'requires', 'description',
-                                'cpp_info', 'aliases', 'custom_content'])
-
-
-def alias_content(name, version, aliased):
-    return textwrap.dedent(f"""\
-        Name: {name}
-        Description: Alias {name} for {aliased}
-        Version: {version}
-        Requires: {aliased}
-        """)
-
-
-class _PCContentGenerator:
+class _PCFilesDeps:
 
     template = textwrap.dedent("""\
         {% for k, v in pc_variables.items() %}
@@ -46,28 +31,106 @@ class _PCContentGenerator:
         {% endif %}
     """)
 
-    def __init__(self, conanfile, dep):
-        self._conanfile = conanfile
+    alias_template = textwrap.dedent("""\
+        Name: {{name}}
+        Description: Alias {{name}} for {{aliased}}
+        Version: {{version}}
+        Requires: {{aliased}}
+    """)
+
+    def __init__(self, pkgconfigdeps, dep, suffix=""):
+        self._conanfile = pkgconfigdeps._conanfile  # noqa
+        self._properties = pkgconfigdeps._properties  # noqa
+        self._transitive_reqs = get_transitive_requires(self._conanfile, dep)
         self._dep = dep
+        self._suffix = suffix
 
-    def _get_prefix_path(self):
-        # If editable, package_folder can be None
-        root_folder = self._dep.recipe_folder if self._dep.package_folder is None \
-            else self._dep.package_folder
-        return root_folder.replace("\\", "/")
+    def _get_aliases(self, dep, pkg_name=None, comp_ref_name=None):
+        def _get_dep_aliases():
+            pkg_aliases = self._get_property("pkg_config_aliases", dep, check_type=list)
+            return pkg_aliases or []
+
+        # TODO: LET'S DEPRECATE ALL THE ALIASES MECHANISM!!
+        if pkg_name is None and comp_ref_name is None:
+            return _get_dep_aliases()
+        if comp_ref_name not in dep.cpp_info.components:
+            # Either foo::foo might be referencing the root cpp_info
+            if (dep.ref.name == comp_ref_name or
+                # Or a "replace_require" is used and cpp_info.requires is the root one, e.g.,
+                # zlib/*: zlib-ng/*, and self.cpp_info.requires = ["zlib::zlib"]
+                (dep.ref.name != pkg_name and pkg_name == comp_ref_name)):
+                return _get_dep_aliases()
+            raise ConanException("Component '{name}::{cname}' not found in '{name}' "
+                                 "package requirement".format(name=dep.ref.name,
+                                                              cname=comp_ref_name))
+        comp_aliases = self._get_property("pkg_config_aliases", dep, comp_ref_name, check_type=list)
+        return comp_aliases or []
+
+    def _get_name(self, dep, pkg_name=None, comp_ref_name=None):
+        def _get_dep_name():
+            dep_name = self._get_property("pkg_config_name", dep) or dep.ref.name
+            return f"{dep_name}{self._suffix}"
+
+        if pkg_name is None and comp_ref_name is None:
+            return _get_dep_name()
+        if comp_ref_name not in dep.cpp_info.components:
+            # Either foo::foo might be referencing the root cpp_info
+            if (dep.ref.name == comp_ref_name or
+                # Or a "replace_require" is used and cpp_info.requires is the root one, e.g.,
+                # zlib/*: zlib-ng/*, and self.cpp_info.requires = ["zlib::zlib"]
+                (dep.ref.name != pkg_name and pkg_name == comp_ref_name)):
+                return _get_dep_name()
+            raise ConanException("Component '{name}::{cname}' not found in '{name}' "
+                                 "package requirement".format(name=dep.ref.name,
+                                                              cname=comp_ref_name))
+        comp_name = self._get_property("pkg_config_name", dep, comp_ref_name)
+        if comp_name:
+            return f"{comp_name}{self._suffix}"
+        else:
+            dep_name = _get_dep_name()
+            # Creating a component name with namespace, e.g., dep-comp1
+            return f"{dep_name}-{comp_ref_name}"
+
+    def _get_property(self, prop, dep, comp_name=None, check_type=None):
+        dep_name = dep.ref.name
+        dep_comp = f"{str(dep_name)}::{comp_name}" if comp_name else f"{str(dep_name)}"
+        try:
+            value = self._properties[f"{dep_comp}{self._suffix}"][prop]
+            if check_type is not None and not isinstance(value, check_type):
+                raise ConanException(
+                    f'The expected type for {prop} is "{check_type.__name__}", but "{type(value).__name__}" was found')
+            return value
+        except KeyError:
+            return dep.cpp_info.get_property(prop, check_type=check_type) if not comp_name \
+                else dep.cpp_info.components[comp_name].get_property(prop, check_type=check_type)
 
-    def _get_pc_variables(self, cpp_info):
+    def _get_pc_variables(self, dep, cpp_info, custom_content=None):
         """
         Get all the freeform variables defined by Conan and
         users (through ``pkg_config_custom_content``). This last ones will override the
         Conan defined variables.
         """
-        prefix_path = self._get_prefix_path()
+        def apply_custom_content():
+            if isinstance(custom_content, dict):
+                pc_variables.update(custom_content)
+            elif custom_content:  # Legacy: custom content is string
+                pc_variable_pattern = re.compile("^(.*)=(.*)")
+                for line in custom_content.splitlines():
+                    match = pc_variable_pattern.match(line)
+                    if match:
+                        key, value = match.group(1).strip(), match.group(2).strip()
+                        pc_variables[key] = value
+
+        # If editable, package_folder can be None
+        prefix_path = (dep.recipe_folder if dep.package_folder is None
+                                   else dep.package_folder).replace("\\", "/")
         pc_variables = {"prefix": prefix_path}
         # Already formatted directories
         pc_variables.update(self._get_formatted_dirs("libdir", cpp_info.libdirs, prefix_path))
         pc_variables.update(self._get_formatted_dirs("includedir", cpp_info.includedirs, prefix_path))
         pc_variables.update(self._get_formatted_dirs("bindir", cpp_info.bindirs, prefix_path))
+        # Get the custom content introduced by user and sanitize it
+        apply_custom_content()
         return pc_variables
 
     @staticmethod
@@ -85,72 +148,28 @@ class _PCContentGenerator:
             ret[var_name] = f"{prefix}{directory}"
         return ret
 
-    def _get_lib_flags(self, libdirvars, cpp_info):
+    def _get_framework_flags(self, cpp_info):
+        # FIXME: GnuDepsFlags used only here. Let's adapt the code and remove this dependency.
+        #        self._conanfile is also used only here.
+        from conan.tools.gnu.gnudeps_flags import GnuDepsFlags
         gnudeps_flags = GnuDepsFlags(self._conanfile, cpp_info)
+        return gnudeps_flags.frameworks + gnudeps_flags.framework_paths
+
+    def _get_lib_flags(self, libdirvars, cpp_info):
+        framework_flags = self._get_framework_flags(cpp_info)
         libdirsflags = ['-L"${%s}"' % d for d in libdirvars]
         system_libs = ["-l%s" % li for li in (cpp_info.libs + cpp_info.system_libs)]
         shared_flags = cpp_info.sharedlinkflags + cpp_info.exelinkflags
-        framework_flags = gnudeps_flags.frameworks + gnudeps_flags.framework_paths
         return " ".join(libdirsflags + system_libs + shared_flags + framework_flags)
 
-    @staticmethod
-    def _get_cflags(includedirvars, cpp_info):
+    def _get_cflags(self, includedirvars, cpp_info):
         includedirsflags = ['-I"${%s}"' % d for d in includedirvars]
         cxxflags = [var.replace('"', '\\"') for var in cpp_info.cxxflags]
         cflags = [var.replace('"', '\\"') for var in cpp_info.cflags]
         defines = ["-D%s" % var.replace('"', '\\"') for var in cpp_info.defines]
         return " ".join(includedirsflags + cxxflags + cflags + defines)
 
-    def _get_context(self, info):
-        def _apply_custom_content(custom_content):
-            if isinstance(custom_content, dict):
-                pc_variables.update(custom_content)
-            elif custom_content:  # Legacy: custom content is string
-                pc_variable_pattern = re.compile("^(.*)=(.*)")
-                for line in custom_content.splitlines():
-                    match = pc_variable_pattern.match(line)
-                    if match:
-                        key, value = match.group(1).strip(), match.group(2).strip()
-                        pc_variables[key] = value
-
-        pc_variables = self._get_pc_variables(info.cpp_info)
-        # Get the custom content introduced by user and sanitize it
-        _apply_custom_content(info.custom_content)
-        context = {
-            "name": info.name,
-            "description": info.description,
-            "version": info.version,
-            "requires": info.requires,
-            "pc_variables": pc_variables,
-            "cflags": self._get_cflags([d for d in pc_variables if d.startswith("includedir")],
-                                       info.cpp_info),
-            "libflags": self._get_lib_flags([d for d in pc_variables if d.startswith("libdir")],
-                                            info.cpp_info)
-            }
-        return context
-
-    def content(self, info):
-        assert isinstance(info, _PCInfo)
-        context = self._get_context(info)
-        template = Template(self.template, trim_blocks=True, lstrip_blocks=True,
-                            undefined=StrictUndefined)
-        return template.render(context)
-
-
-class _PCGenerator:
-
-    def __init__(self, pkgconfigdeps, require, dep):
-        self._conanfile = pkgconfigdeps._conanfile  # noqa
-        self._properties = pkgconfigdeps._properties  # noqa
-        self._require = require
-        self._dep = dep
-        self._transitive_reqs = get_transitive_requires(self._conanfile, dep)
-        self._is_build_context = require.build
-        self._build_context_folder = pkgconfigdeps.build_context_folder
-        self._suffix = pkgconfigdeps.build_context_suffix.get(require.ref.name, "") \
-            if self._is_build_context else ""
-
-    def _get_cpp_info_requires_names(self, cpp_info):
+    def _get_component_requirement_names(self, cpp_info):
         """
         Get all the pkg-config valid names from the requirements ones given a CppInfo object.
 
@@ -182,68 +201,11 @@ class _PCGenerator:
                     continue  # If the dependency is not in the transitive, might be skipped
             else:  # For instance, dep == "hello/1.0" and req == "hello::cmp1" -> hello == hello
                 req_conanfile = self._dep
-            comp_name = self._get_component_name(req_conanfile, pkg_ref_name, comp_ref_name)
-            if not comp_name:
-                pkg_name = self._get_package_name(req_conanfile)
-                # Creating a component name with namespace, e.g., dep-comp1
-                comp_name = self._get_name_with_namespace(pkg_name, comp_ref_name)
+            comp_name = self._get_name(req_conanfile, pkg_ref_name, comp_ref_name)
             ret.append(comp_name)
         return ret
 
-    def _components_info(self):
-        """
-        Get the whole package and its components information like their own requires, names and even
-        the cpp_info for each component.
-
-        :return: `list` of `_PCInfo` objects with all the components information
-        """
-        pkg_name = self._get_package_name(self._dep)
-        components_info = []
-        # Loop through all the package's components
-        for comp_ref_name, cpp_info in self._dep.cpp_info.get_sorted_components().items():
-            # At first, let's check if we have defined some components requires, e.g., "dep::cmp1"
-            comp_requires_names = self._get_cpp_info_requires_names(cpp_info)
-            comp_name = self._get_component_name(self._dep, pkg_name, comp_ref_name)
-            if not comp_name:
-                comp_name = self._get_name_with_namespace(pkg_name, comp_ref_name)
-                comp_description = f"Conan component: {comp_name}"
-            else:
-                comp_description = f"Conan component: {pkg_name}-{comp_name}"
-            comp_aliases = self._get_component_aliases(self._dep, pkg_name, comp_ref_name)
-            comp_version = (self.get_property("component_version", self._dep, comp_ref_name) or
-                            self.get_property("system_package_version", self._dep, comp_ref_name) or
-                            self._dep.ref.version)
-            comp_custom_content = self.get_property("pkg_config_custom_content", self._dep, comp_ref_name)
-            # Save each component information
-            components_info.append(_PCInfo(comp_name, comp_version, comp_requires_names, comp_description,
-                                           cpp_info, comp_aliases, comp_custom_content))
-        return components_info
-
-    def _package_info(self):
-        """
-        Get the whole package information
-
-        :return: `_PCInfo` object with the package information
-        """
-        pkg_name = self._get_package_name(self._dep)
-        # At first, let's check if we have defined some global requires, e.g., "other::cmp1"
-        requires = self._get_cpp_info_requires_names(self._dep.cpp_info)
-        # If we have found some component requires it would be enough
-        if not requires:
-            # If no requires were found, let's try to get all the direct visible dependencies,
-            # e.g., requires = "other_pkg/1.0"
-            requires = [self._get_package_name(req)
-                        for req in self._transitive_reqs.values()]
-        description = "Conan package: %s" % pkg_name
-        pkg_version = (self.get_property("system_package_version", self._dep)
-                       or self._dep.ref.version)
-        aliases = self._get_package_aliases(self._dep)
-        cpp_info = self._dep.cpp_info
-        custom_content = self.get_property("pkg_config_custom_content", self._dep)
-        return _PCInfo(pkg_name, pkg_version, requires, description, cpp_info, aliases, custom_content)
-
-    @property
-    def pc_files(self):
+    def items(self):
         """
         Get all the PC files and contents for any dependency:
 
@@ -260,105 +222,88 @@ class _PCGenerator:
 
         * Apart from those PC files, if there are any aliases declared, they will be created too.
         """
-        def _file_name(name):
-            # If no suffix is defined, we can save the *.pc file in the build_context_folder
-            build = self._is_build_context and self._build_context_folder and not self._suffix
-            # Issue: https://github.com/conan-io/conan/issues/12342
-            # Issue: https://github.com/conan-io/conan/issues/14935
-            return f"{self._build_context_folder}/{name}.pc" if build else f"{name}.pc"
-
-        def _add_pc_files(pc_info):
-            content_generator = _PCContentGenerator(self._conanfile, self._dep)
-            result = {_file_name(pc_info.name): content_generator.content(pc_info)}
-            for alias in pc_info.aliases:
-                result[_file_name(alias)] = alias_content(alias, pc_info.version, pc_info.name)
-            return result
-
-        # If the package has no components, then we have to calculate only the root pc file
-        if not self._dep.cpp_info.has_components:
-            pkg_pc_info = self._package_info()
-            return _add_pc_files(pkg_pc_info)
-
+        pc_files = {}
+        pc_alias_files = {}
+        pkg_name = self._get_name(self._dep)
         # First, let's load all the components PC files
         # Loop through all the package's components
-        pc_files = {}
-        pkg_requires = []
-        for comp_pc_info in self._components_info():
-            pc_files.update(_add_pc_files(comp_pc_info))
-            # Saving components name as the package requires
-            pkg_requires.append(comp_pc_info.name)
-
+        for comp_ref_name, comp_cpp_info in self._dep.cpp_info.get_sorted_components().items():
+            # At first, let's check if we have defined some components requires, e.g., "dep::cmp1"
+            comp_requires = self._get_component_requirement_names(comp_cpp_info)
+            comp_name = self._get_name(self._dep, pkg_name, comp_ref_name)
+            version = (self._get_property("component_version", self._dep, comp_ref_name) or
+                       self._get_property("system_package_version", self._dep, comp_ref_name) or
+                       self._dep.ref.version)
+            custom_content = self._get_property("pkg_config_custom_content", self._dep, comp_ref_name)
+            pc_variables = self._get_pc_variables(self._dep, comp_cpp_info, custom_content)
+            pc_context = {
+                "name": comp_name,
+                "description": f"Conan component: {comp_name}",
+                "version": version,
+                "requires": comp_requires,
+                "pc_variables": pc_variables,
+                "cflags": self._get_cflags([d for d in pc_variables if d.startswith("includedir")],
+                                           comp_cpp_info),
+                "libflags": self._get_lib_flags([d for d in pc_variables if d.startswith("libdir")],
+                                                comp_cpp_info)
+            }
+            pc_files[comp_name] = self._get_pc_content(pc_context)
+            # Aliases
+            for alias in self._get_aliases(self._dep, pkg_name, comp_ref_name):
+                pc_alias_files[alias] = self._get_alias_pc_content({
+                    "name": alias,
+                    "version": version,
+                    "aliased": comp_name
+                })
         # Second, let's load the root package's PC file ONLY
         # if it does not already exist in components one
         # Issue related: https://github.com/conan-io/conan/issues/10341
-        pkg_name = self._get_package_name(self._dep)
-        pkg_version = (self.get_property("system_package_version", self._dep)
+        if pkg_name not in pc_files:
+            cpp_info = self._dep.cpp_info
+            # At first, let's check if we have defined some global requires, e.g., "other::cmp1"
+            # Note: If DEP has components,, they'll be the requirements == pc_files.keys()
+            requires = list(pc_files.keys()) or self._get_component_requirement_names(cpp_info)
+            # If we have found some component requirements it would be enough
+            if not requires:
+                # If no requires were found, let's try to get all the direct visible dependencies,
+                # e.g., requires = "other_pkg/1.0"
+                requires = [self._get_name(req) for req in self._transitive_reqs.values()]
+            version = (self._get_property("system_package_version", self._dep)
                        or self._dep.ref.version)
-        if f"{pkg_name}.pc" not in pc_files:
-            package_info = _PCInfo(pkg_name,
-                                   pkg_version,
-                                   pkg_requires, f"Conan package: {pkg_name}",
-                                   self._dep.cpp_info, self._get_package_aliases(self._dep),
-                                   self.get_property("pkg_config_custom_content", self._dep))
-            pc_files.update(_add_pc_files(package_info))
-        return pc_files
-
-    @staticmethod
-    def _get_name_with_namespace(namespace, name):
-        """
-        Build a name with a namespace, e.g., openssl-crypto
-        """
-        return f"{namespace}-{name}"
-
-    def _get_package_aliases(self, dep):
-        pkg_aliases = self.get_property("pkg_config_aliases", dep, check_type=list)
-        return pkg_aliases or []
-
-    def _get_component_aliases(self, dep, pkg_name, comp_name):
-        # TODO: LET'S DEPRECATE ALL THE ALIASES MECHANISM!!
-        if comp_name not in dep.cpp_info.components:
-            # Either foo::foo might be referencing the root cpp_info
-            if (dep.ref.name == comp_name or
-                # Or a "replace_require" is used and cpp_info.requires is the root one, e.g.,
-                # zlib/*: zlib-ng/*, and self.cpp_info.requires = ["zlib::zlib"]
-                (dep.ref.name != pkg_name and pkg_name == comp_name)):
-                return self._get_package_aliases(dep)
-            raise ConanException("Component '{name}::{cname}' not found in '{name}' "
-                                 "package requirement".format(name=dep.ref.name,
-                                                              cname=comp_name))
-        comp_aliases = self.get_property("pkg_config_aliases", dep, comp_name, check_type=list)
-        return comp_aliases or []
-
-    def _get_package_name(self, dep):
-        pkg_name = self.get_property("pkg_config_name", dep) or dep.ref.name
-        return f"{pkg_name}{self._suffix}"
-
-    def _get_component_name(self, dep, pkg_name, comp_name):
-        if comp_name not in dep.cpp_info.components:
-            # Either foo::foo might be referencing the root cpp_info
-            if (dep.ref.name == comp_name or
-                # Or a "replace_require" is used and cpp_info.requires is the root one, e.g.,
-                # zlib/*: zlib-ng/*, and self.cpp_info.requires = ["zlib::zlib"]
-                (dep.ref.name != pkg_name and pkg_name == comp_name)):
-                return self._get_package_name(dep)
-            raise ConanException("Component '{name}::{cname}' not found in '{name}' "
-                                 "package requirement".format(name=dep.ref.name,
-                                                              cname=comp_name))
-        comp_name = self.get_property("pkg_config_name", dep, comp_name)
-        return f"{comp_name}{self._suffix}" if comp_name else None
+            custom_content = self._get_property("pkg_config_custom_content", self._dep)
+            pc_variables = self._get_pc_variables(self._dep, cpp_info, custom_content)
+            pc_context = {
+                "name": pkg_name,
+                "description": f"Conan package: {pkg_name}",
+                "version": version,
+                "requires": requires,
+                "pc_variables": pc_variables,
+                "cflags": self._get_cflags([d for d in pc_variables if d.startswith("includedir")],
+                                           cpp_info),
+                "libflags": self._get_lib_flags([d for d in pc_variables if d.startswith("libdir")],
+                                                cpp_info)
+            }
+            pc_files[pkg_name] = self._get_pc_content(pc_context)
+            # Aliases
+            for alias in self._get_aliases(self._dep):
+                pc_alias_files[alias] = self._get_alias_pc_content({
+                    "name": alias,
+                    "version": version,
+                    "aliased": pkg_name
+                })
+        # Adding the aliases
+        pc_files.update(pc_alias_files)
+        return pc_files.items()
+
+    def _get_pc_content(self, context):
+        template = Template(self.template, trim_blocks=True, lstrip_blocks=True,
+                            undefined=StrictUndefined)
+        return template.render(context)
 
-    def get_property(self, prop, dep, comp_name=None, check_type=None):
-        dep_name = dep.ref.name
-        dep_comp = f"{str(dep_name)}::{comp_name}" if comp_name else f"{str(dep_name)}"
-        try:
-            value = self._properties[f"{dep_comp}{self._suffix}"][prop]
-            if check_type is not None and not isinstance(value, check_type):
-                raise ConanException(
-                    f'The expected type for {prop} is "{check_type.__name__}", but "{type(value).__name__}" was found')
-            return value
-        except KeyError:
-            return dep.cpp_info.get_property(prop, check_type=check_type) if not comp_name \
-                else dep.cpp_info.components[comp_name].get_property(prop, check_type=check_type)
+    def _get_alias_pc_content(self, context):
+        template = Template(self.alias_template, trim_blocks=True, lstrip_blocks=True,
+                            undefined=StrictUndefined, keep_trailing_newline=True)
+        return template.render(context)
 
 
 class PkgConfigDeps:
@@ -382,30 +327,7 @@ class PkgConfigDeps:
         self.build_context_folder = None  # Keeping backward-compatibility
         self._properties = {}
 
-    def _validate_build_requires(self, host_req, build_req):
-        """
-        Check if any package exists at host and build context at the same time, and
-        it doesn't have any suffix to avoid any name collisions
-
-        :param host_req: list of host requires
-        :param build_req: list of build requires
-        """
-        activated_br = {r.ref.name for r in build_req.values()
-                        if r.ref.name in self.build_context_activated}
-        common_names = {r.ref.name for r in host_req.values()}.intersection(activated_br)
-        without_suffixes = [common_name for common_name in common_names
-                            if not self.build_context_suffix.get(common_name)]
-        if without_suffixes:
-            raise ConanException(f"The packages {without_suffixes} exist both as 'require' and as"
-                                 f" 'build require'. You need to specify a suffix using the "
-                                 f"'build_context_suffix' attribute at the PkgConfigDeps generator.")
-
-    @property
-    def content(self):
-        """
-        Get all the .pc files content
-        """
-        pc_files = {}
+    def _get_dependencies(self):
         # Get all the dependencies
         host_req = self._conanfile.dependencies.host
         build_req = self._conanfile.dependencies.build  # tool_requires
@@ -416,31 +338,51 @@ class PkgConfigDeps:
         if self.build_context_folder is None:  # Legacy flow
             if self.build_context_suffix:
                 # deprecation warning
-                self._conanfile.output.warning("PkgConfigDeps.build_context_suffix attribute has been "
-                                               "deprecated. Use PkgConfigDeps.build_context_folder instead.")
+                self._conanfile.output.warning(
+                    "PkgConfigDeps.build_context_suffix attribute has been "
+                    "deprecated. Use PkgConfigDeps.build_context_folder instead."
+                )
             # Check if it exists both as require and as build require without a suffix
-            self._validate_build_requires(host_req, build_req)
+            activated_br = {r.ref.name for r in build_req.values()
+                            if r.ref.name in self.build_context_activated}
+            common_names = {r.ref.name for r in host_req.values()}.intersection(activated_br)
+            without_suffixes = [common_name for common_name in common_names
+                                if not self.build_context_suffix.get(common_name)]
+            if without_suffixes:
+                raise ConanException(
+                    f"The packages {without_suffixes} exist both as 'require' and as"
+                    f" 'build require'. You need to specify a suffix using the "
+                    f"'build_context_suffix' attribute at the PkgConfigDeps generator.")
         elif self.build_context_folder is not None and self.build_context_suffix:
-            raise ConanException("It's not allowed to define both PkgConfigDeps.build_context_folder "
-                                 "and PkgConfigDeps.build_context_suffix (deprecated).")
+            raise ConanException(
+                "It's not allowed to define both PkgConfigDeps.build_context_folder "
+                "and PkgConfigDeps.build_context_suffix (deprecated).")
 
         for require, dep in list(host_req.items()) + list(build_req.items()) + list(test_req.items()):
             # Filter the build_requires not activated with PkgConfigDeps.build_context_activated
             if require.build and dep.ref.name not in self.build_context_activated:
                 continue
-            # Save all the *.pc files and their contents
-            pc_files.update(_PCGenerator(self, require, dep).pc_files)
-        return pc_files
+            yield require, dep
 
     def generate(self):
         """
         Save all the `*.pc` files
         """
+        def _pc_file_name(name_, is_build_context=False, has_suffix=False):
+            # If no suffix is defined, we can save the *.pc file in the build_context_folder
+            build = is_build_context and self.build_context_folder and not has_suffix
+            # Issue: https://github.com/conan-io/conan/issues/12342
+            # Issue: https://github.com/conan-io/conan/issues/14935
+            return f"{self.build_context_folder}/{name_}.pc" if build else f"{name_}.pc"
+
         check_duplicated_generator(self, self._conanfile)
-        # Current directory is the generators_folder
-        generator_files = self.content
-        for generator_file, content in generator_files.items():
-            save(generator_file, content)
+        for require, dep in self._get_dependencies():
+            suffix = self.build_context_suffix.get(require.ref.name, "") if require.build else ""
+            # Save all the *.pc files and their contents
+            for name, content in _PCFilesDeps(self, dep, suffix=suffix).items():
+                pc_name = _pc_file_name(name, is_build_context=require.build,
+                                        has_suffix=bool(suffix))
+                save(pc_name, content)
 
     def set_property(self, dep, prop, value):
         """


Test patch:
diff --git a/test/functional/toolchains/gnu/test_pkgconfigdeps.py b/test/functional/toolchains/gnu/test_pkgconfigdeps.py
index 0a0c2afec..501d99695 100644
--- a/test/functional/toolchains/gnu/test_pkgconfigdeps.py
+++ b/test/functional/toolchains/gnu/test_pkgconfigdeps.py
@@ -129,4 +129,4 @@ def test_pkgconfigdeps_component_matches_package_name():
     client.run("export-pkg .")
     client.run("install --requires=hello/0.1 -g PkgConfigDeps")
     content = client.load("hello.pc")
-    assert "Conan component: hello-hello" in content
+    assert "Conan component: hello" in content
diff --git a/test/integration/configuration/test_profile_jinja.py b/test/integration/configuration/test_profile_jinja.py
index d8af563a1..ac3ca66af 100644
--- a/test/integration/configuration/test_profile_jinja.py
+++ b/test/integration/configuration/test_profile_jinja.py
@@ -10,7 +10,9 @@ from conan.test.utils.env import environment_update
 
 def test_profile_template():
     client = TestClient()
-    tpl = textwrap.dedent("""
+    tpl = textwrap.dedent("""\
+        # This is just to check that subprocess is injected in the render and can be used
+        {% set check_output = subprocess.check_output %}
         [settings]
         os = {{ {"Darwin": "Macos"}.get(platform.system(), platform.system()) }}
         build_type = {{ os.getenv("MY_BUILD_TYPE") }}
diff --git a/test/integration/toolchains/gnu/test_pkgconfigdeps.py b/test/integration/toolchains/gnu/test_pkgconfigdeps.py
index 29ab7a21d..430e34b36 100644
--- a/test/integration/toolchains/gnu/test_pkgconfigdeps.py
+++ b/test/integration/toolchains/gnu/test_pkgconfigdeps.py
@@ -491,7 +491,7 @@ def test_pkg_config_name_full_aliases():
 
     pc_content = client.load("compo1.pc")
     prefix = pc_content.splitlines()[0]
-    assert "Description: Conan component: pkg_other_name-compo1" in pc_content
+    assert "Description: Conan component: compo1" in pc_content
     assert "Requires" not in pc_content
 
     pc_content = client.load("compo1_alias.pc")