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

Feature/cps #16054

Draft
wants to merge 5 commits into
base: develop2
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions conan/tools/cps/__init__.py
@@ -0,0 +1 @@
from conan.tools.cps.cps_deps import CPSDeps
117 changes: 117 additions & 0 deletions conan/tools/cps/cps_deps.py
@@ -0,0 +1,117 @@
from conan.tools.files import save

import glob
import json
import os


class CPSDeps:
def __init__(self, conanfile):
self.conanfile = conanfile

_package_type_map = {
"shared-library": "dylib",
"static-library": "archive",
"header-library": "interface"
}

def find_library(self, libdirs, bindirs, name, shared, dll):
libdirs = [x.replace("\\", "/") for x in libdirs]
bindirs = [x.replace("\\", "/") for x in bindirs]
libname = name[0] # assume one library per component
if shared and not dll:
patterns = [f"lib{libname}.so", f"lib{libname}.so.*", f"lib{libname}.dylib",
f"lib{libname}.*dylib", f"{libname}.lib"]
elif shared and dll:
patterns = [f"{libname}.dll"]
else:
patterns = [f"lib{libname}.a", f"{libname}.lib"]

matches = set()
search_dirs = bindirs if self.conanfile.settings.os == "Windows" and shared and dll else libdirs
for folder in search_dirs:
for pattern in patterns:
glob_expr = f"{folder}/{pattern}"
matches.update(glob.glob(glob_expr))

if len(matches) == 1:
return next(iter(matches))
elif len(matches) >= 1:
# assume at least one is not a symlink
return [x for x in list(matches) if not os.path.islink(x)][0]
else:
self.conanfile.output.error(f"[CPSDeps] Could not locate library: {libname}")
return None

def _component(self, package_type, cpp_info, build_type):
component = {}
component["Type"] = self._package_type_map.get(str(package_type), "unknown")
component["Definitions"] = cpp_info.defines
component["Includes"] = [x.replace("\\", "/") for x in cpp_info.includedirs]

if not cpp_info.libs: # No compiled libraries, header-only
return component
is_shared = package_type == "shared-library"
if is_shared and self.conanfile.settings.os == "Windows":
dll_location = self.find_library(cpp_info.libdirs, cpp_info.bindirs,
cpp_info.libs, is_shared, dll=True)
import_library = self.find_library(cpp_info.libdirs, cpp_info.bindirs,
cpp_info.libs, is_shared, dll=False)
locations = {'Location': dll_location,
'Link-Location': import_library}
component["Configurations"] = {build_type: locations} # noqa
elif package_type == "static-library":
library_location = self.find_library(cpp_info.libdirs, cpp_info.bindirs,
cpp_info.libs, is_shared, dll=False)
component["Configurations"] = {build_type: {'Location': library_location}}

return component

def generate(self):
self.conanfile.output.info(f"[CPSDeps] generators folder {self.conanfile.generators_folder}")
deps = self.conanfile.dependencies.host.items()
mapping = {}
for _, dep in deps:
self.conanfile.output.info(f"[CPSDeps]: dep {dep.ref.name}")

cps_in_package = os.path.join(dep.package_folder, f"{dep.ref.name}.cps")
if os.path.exists(cps_in_package):
mapping[dep.ref.name] = cps_in_package
continue

cps = {"Cps-Version": "0.8.1",
"Name": dep.ref.name,
"Version": str(dep.ref.version)}

build_type = str(self.conanfile.settings.build_type).lower()
cps["Configurations"] = [build_type]

if not dep.cpp_info.has_components:
# single component, called same as library
component = self._component(dep.package_type, dep.cpp_info, build_type)
if dep.dependencies:
for transitive_dep in dep.dependencies.items():
dep_name = transitive_dep[0].ref.name
component["Requires"] = [f"{dep_name}:{dep_name}"]

cps["Default-Components"] = [f"{dep.ref.name}"]
cps["Components"] = {f"{dep.ref.name}": component}
else:
sorted_comps = dep.cpp_info.get_sorted_components()
for comp_name, comp in sorted_comps.items():
component = self._component(dep.package_type, comp, build_type)
cps.setdefault("Components", {})[comp_name] = component
cps["Default-Components"] = [comp_name for comp_name in sorted_comps]

output_file = os.path.join(self.conanfile.generators_folder, f"{dep.ref.name}.cps")
cps_json = json.dumps(cps, indent=4)
save(self.conanfile, output_file, cps_json)
mapping[dep.ref.name] = output_file

name = ["cpsmap",
self.conanfile.settings.get_safe("arch"),
self.conanfile.settings.get_safe("build_type"),
self.conanfile.options.get_safe("shared")]
name = "-".join([f for f in name if f]) + ".json"
self.conanfile.output.info(f"Generating CPS mapping file: {name}")
save(self.conanfile, name, json.dumps(mapping, indent=2))
3 changes: 2 additions & 1 deletion conans/client/generators/__init__.py
Expand Up @@ -30,7 +30,8 @@
"XcodeToolchain": "conan.tools.apple",
"PremakeDeps": "conan.tools.premake",
"MakeDeps": "conan.tools.gnu",
"SConsDeps": "conan.tools.scons"
"SConsDeps": "conan.tools.scons",
"CPSDeps": "conan.tools.cps"
}


Expand Down
Empty file.
68 changes: 68 additions & 0 deletions conans/test/integration/cps/test_cps.py
@@ -0,0 +1,68 @@
import json
import os
import textwrap

from conans.test.assets.genconanfile import GenConanfile
from conans.test.utils.tools import TestClient


def test_cps():
c = TestClient()
c.save({"pkg/conanfile.py": GenConanfile("pkg", "0.1")})
c.run("create pkg")

c.run("install --requires=pkg/0.1 -s arch=x86_64 -g CPSDeps")
pkg = json.loads(c.load("pkg.cps"))
print(json.dumps(pkg, indent=2))
assert pkg["Name"] == "pkg"
assert pkg["Version"] == "0.1"
assert pkg["Configurations"] == ["release"]
assert pkg["Name"] == "pkg"
assert pkg["Default-Components"] == ["pkg"]
pkg_comp = pkg["Components"]["pkg"]
mapping = json.loads(c.load("cpsmap-x86_64-Release.json"))
for _, path_cps in mapping.items():
assert os.path.exists(path_cps)


def test_cps_in_pkg():
c = TestClient()
cps = textwrap.dedent("""
{
"Cps-Version": "0.8.1",
"Name": "pkg",
"Version": "0.1",
"Configurations": ["release"],
"Default-Components": ["pkg"],
"Components": {
"pkg": { "Type": "unknown", "Definitions": [],
"Includes": ["include"]}
}
}
""")
cps = "".join(cps.splitlines())
conanfile = textwrap.dedent(f"""
import os
from conan.tools.files import save
from conan import ConanFile
class Pkg(ConanFile):
name = "mypkg"
version = "0.1"

def package(self):
cps = '{cps}'
cps_path = os.path.join(self.package_folder, "mypkg.cps")
save(self, cps_path, cps)
""")
c.save({"pkg/conanfile.py": conanfile})
c.run("create pkg")

c.run("install --requires=mypkg/0.1 -s arch=x86_64 -g CPSDeps")
print(c.out)

mapping = json.loads(c.load("cpsmap-x86_64-Release.json"))
print(json.dumps(mapping, indent=2))
for _, path_cps in mapping.items():
assert os.path.exists(path_cps)

assert not os.path.exists(os.path.join(c.current_folder, "mypkg.cps"))