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

Add SConsDeps generator #14713

Merged
merged 13 commits into from
Sep 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions conan/tools/scons/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from conan.tools.scons.sconsdeps import SConsDeps
62 changes: 62 additions & 0 deletions conan/tools/scons/sconsdeps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from jinja2 import Template
from conan.tools._check_build_profile import check_using_build_profile
from conans.model.new_build_info import NewCppInfo
from conans.util.files import save


class SConsDeps:
def __init__(self, conanfile):
self._conanfile = conanfile
self._ordered_deps = None
self._generator_file = 'SConscript_conandeps'
check_using_build_profile(self._conanfile)

@property
def ordered_deps(self):
if self._ordered_deps is None:
deps = self._conanfile.dependencies.host.topological_sort
self._ordered_deps = [dep for dep in reversed(deps.values())]
return self._ordered_deps

def _get_cpp_info(self):
ret = NewCppInfo()
for dep in self.ordered_deps:
dep_cppinfo = dep.cpp_info.aggregated_components()
ret.merge(dep_cppinfo)
return ret

def generate(self):
save(self._generator_file, self._content)

@property
def _content(self):
template = Template("""
"{{dep_name}}" : {
"CPPPATH" : {{info.includedirs or []}},
"LIBPATH" : {{info.libdirs or []}},
"BINPATH" : {{info.bindirs or []}},
"LIBS" : {{(info.libs or []) + (info.system_libs or [])}},
"FRAMEWORKS" : {{info.frameworks or []}},
"FRAMEWORKPATH" : {{info.frameworkdirs or []}},
"CPPDEFINES" : {{info.defines or []}},
"CXXFLAGS" : {{info.cxxflags or []}},
"CCFLAGS" : {{info.cflags or []}},
"SHLINKFLAGS" : {{info.sharedlinkflags or []}},
"LINKFLAGS" : {{info.exelinkflags or []}},
},
{% if dep_version is not none %}"{{dep_name}}_version" : "{{dep_version}}",{% endif %}
""")
sections = ["conandeps = {\n"]
all_flags = template.render(dep_name="conandeps", dep_version=None,
info=self._get_cpp_info())
sections.append(all_flags)

# TODO: Add here in 2.0 the "skip": False trait
host_req = self._conanfile.dependencies.filter({"build": False}).values()
for dep in host_req:
dep_flags = template.render(dep_name=dep.ref.name, dep_version=dep.ref.version,
info=dep.cpp_info)
sections.append(dep_flags)
sections.append("}\n")
sections.append("Return('conandeps')\n")
return "\n".join(sections)
5 changes: 4 additions & 1 deletion conans/client/generators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def __init__(self):
"VirtualRunEnv", "VirtualBuildEnv", "AutotoolsDeps",
"AutotoolsToolchain", "BazelDeps", "BazelToolchain", "PkgConfigDeps",
"VCVars", "IntelCC", "XcodeDeps", "PremakeDeps", "XcodeToolchain",
"MesonDeps", "NMakeToolchain", "NMakeDeps"]
"MesonDeps", "NMakeToolchain", "NMakeDeps", "SConsDeps"]

def add(self, name, generator_class, custom=False):
if name not in self._generators or custom:
Expand Down Expand Up @@ -156,6 +156,9 @@ def _new_generator(self, generator_name, output):
elif generator_name == "NMakeDeps":
from conan.tools.microsoft import NMakeDeps
return NMakeDeps
elif generator_name == "SConsDeps":
from conan.tools.scons import SConsDeps
return SConsDeps
else:
raise ConanException("Internal Conan error: Generator '{}' "
"not commplete".format(generator_name))
Expand Down
3 changes: 3 additions & 0 deletions conans/test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@
"system": {"path": {'Windows': 'C:/bazel/bin',
"Darwin": '/Users/jenkins/bin'}},
},
'scons': {
"default": "system"
},
'premake': {
"exe": "premake5",
"default": "5.0.0",
Expand Down
180 changes: 180 additions & 0 deletions conans/test/functional/toolchains/scons/test_sconsdeps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import platform
import textwrap

import pytest

from conans.test.utils.tools import TestClient


@pytest.mark.skipif(platform.system() != "Linux", reason="SCons functional tests"
"only for Linux")
@pytest.mark.tool("scons")
def test_sconsdeps():
client = TestClient(path_with_spaces=False)

conanfile = textwrap.dedent("""\
import os

from conan import ConanFile
from conan.tools.files import copy


class helloConan(ConanFile):
name = "hello"
version = "1.0"
settings = "os", "compiler", "build_type", "arch"

exports_sources = "src/*"

# TODO: check what would be the correct layout and how to interact with
# SCons scripts
def layout(self):
self.folders.source = "src"

def build(self):
debug_opt = '--debug-build' if self.settings.build_type == 'Debug' else ''
self.run(f'scons -C {self.folders.source} {debug_opt}')

def package(self):
copy(self, pattern="*.h", dst=os.path.join(self.package_folder, "include"), src=os.path.join(self.source_folder),)
copy(self, "*.lib", src=self.source_folder, dst=os.path.join(self.package_folder, "lib"), keep_path=False)
copy(self, "*.a", src=self.source_folder, dst=os.path.join(self.package_folder, "lib"), keep_path=False)

def package_info(self):
self.cpp_info.libs = ["hello"]

""")

hello_cpp = textwrap.dedent("""\
#include <iostream>
#include "hello.h"

void hello(){
#ifdef NDEBUG
std::cout << "Hello World Release!" <<std::endl;
#else
std::cout << "Hello World Debug!" <<std::endl;
#endif
}
""")

hello_h = textwrap.dedent("""\
#pragma once

#ifdef WIN32
#define HELLO_EXPORT __declspec(dllexport)
#else
#define HELLO_EXPORT
#endif

HELLO_EXPORT void hello();
""")

sconscript = textwrap.dedent("""\
import sys

AddOption('--debug-build', action='store_true', help='debug build')

env = Environment(TARGET_ARCH="x86_64")

is_debug = GetOption('debug_build')
is_release = not is_debug

if not is_debug:
env.Append(CPPDEFINES="NDEBUG")

if is_debug:
env.Append(CXXFLAGS = '-g -ggdb')
else:
env.Append(CXXFLAGS = '-O2')
env.Append(LINKFLAGS = '-O2')

env.Library("hello", "hello.cpp")
""")

sconstruct = textwrap.dedent("""\
SConscript('SConscript', variant_dir='build', duplicate = False)
""")

t_sconscript = textwrap.dedent("""\
import sys

AddOption('--debug-build', action='store_true', help='debug build')

env = Environment(TARGET_ARCH="x86_64")

is_debug = GetOption('debug_build')
is_release = not is_debug

if not is_debug:
env.Append(CPPDEFINES="NDEBUG")

if is_debug:
env.Append(CXXFLAGS = '-g -ggdb')
else:
env.Append(CXXFLAGS = '-O2')
env.Append(LINKFLAGS = '-O2')

build_path_relative_to_sconstruct = Dir('.').path

conandeps = SConscript('./SConscript_conandeps')

flags = conandeps["conandeps"]
env.MergeFlags(flags)

env.Program("main", "main.cpp")
""")

t_sconstruct = textwrap.dedent("""\
SConscript('SConscript', variant_dir='build', duplicate = False)
""")
t_conanfile = textwrap.dedent("""\
import os

from conan import ConanFile

class helloTestConan(ConanFile):
settings = "os", "compiler", "build_type", "arch"
generators = "SConsDeps"
apply_env = False
test_type = "explicit"

def requirements(self):
self.requires(self.tested_reference_str)

def build(self):
debug_opt = '--debug-build' if self.settings.build_type == 'Debug' else ''
self.run(f'scons {debug_opt}')

# TODO: check how to setup layout and scons
def layout(self):
self.folders.source = "."
self.folders.generators = self.folders.source
self.cpp.build.bindirs = ["build"]

def test(self):
cmd = os.path.join(self.cpp.build.bindirs[0], "main")
self.run(cmd, env="conanrun")
""")

t_main_cpp = textwrap.dedent("""\
#include "hello.h"

int main() {
hello();
}
""")

client.save({"conanfile.py": conanfile,
"src/hello.cpp": hello_cpp,
"src/hello.h": hello_h,
"src/SConscript": sconscript,
"src/SConstruct": sconstruct,
"test_package/SConscript": t_sconscript,
"test_package/SConstruct": t_sconstruct,
"test_package/conanfile.py": t_conanfile,
"test_package/main.cpp": t_main_cpp,
})

client.run("create .")
assert "Hello World Release!" in client.out
Empty file.
99 changes: 99 additions & 0 deletions conans/test/integration/toolchains/scons/test_scondeps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import re
import textwrap

from conans.test.utils.tools import TestClient


def test_sconsdeps():
dep = textwrap.dedent("""
from conan import ConanFile
class ExampleConanIntegration(ConanFile):
name = "{dep}"
version = "0.1"
def package_info(self):
self.cpp_info.includedirs = ["{dep}_includedir"]
self.cpp_info.libdirs = ["{dep}_libdir"]
self.cpp_info.bindirs = ["{dep}_bindir"]
self.cpp_info.libs = ["{dep}_lib"]
self.cpp_info.frameworks = ["{dep}_frameworks"]
self.cpp_info.frameworkdirs = ["{dep}_frameworkdirs"]
self.cpp_info.defines = ["{dep}_defines"]
self.cpp_info.cxxflags = ["{dep}_cxxflags"]
self.cpp_info.cflags = ["{dep}_cflags"]
self.cpp_info.sharedlinkflags = ["{dep}_sharedlinkflags"]
self.cpp_info.exelinkflags = ["{dep}_exelinkflags"]
""")

consumer = textwrap.dedent("""
from conan import ConanFile
from conan.tools.layout import basic_layout

class ExampleConanIntegration(ConanFile):
generators = 'SConsDeps'
requires = 'dep1/0.1', 'dep2/0.1'
""")

c = TestClient()
c.save({"dep1/conanfile.py": dep.format(dep="dep1"),
"dep2/conanfile.py": dep.format(dep="dep2"),
"consumer/conanfile.py": consumer})
c.run("create dep1")
c.run("create dep2")
c.run("install consumer")
sconsdeps = c.load("SConscript_conandeps")

# remove all cache paths from the output but the last component
def clean_paths(text):
text = text.replace("\\", "/")
pattern = r"'[A-Za-z]?[:]?[/]?[^']+/([^'/]+)'"
return re.sub(pattern, r"'\1'", text)

expected_content = ["""
"conandeps" : {
"CPPPATH" : ['dep2_includedir', 'dep1_includedir'],
"LIBPATH" : ['dep2_libdir', 'dep1_libdir'],
"BINPATH" : ['dep2_bindir', 'dep1_bindir'],
"LIBS" : ['dep2_lib', 'dep1_lib'],
"FRAMEWORKS" : ['dep2_frameworks', 'dep1_frameworks'],
"FRAMEWORKPATH" : ['dep2_frameworkdirs', 'dep1_frameworkdirs'],
"CPPDEFINES" : ['dep2_defines', 'dep1_defines'],
"CXXFLAGS" : ['dep2_cxxflags', 'dep1_cxxflags'],
"CCFLAGS" : ['dep2_cflags', 'dep1_cflags'],
"SHLINKFLAGS" : ['dep2_sharedlinkflags', 'dep1_sharedlinkflags'],
"LINKFLAGS" : ['dep2_exelinkflags', 'dep1_exelinkflags'],
},
""", """
"dep1" : {
"CPPPATH" : ['dep1_includedir'],
"LIBPATH" : ['dep1_libdir'],
"BINPATH" : ['dep1_bindir'],
"LIBS" : ['dep1_lib'],
"FRAMEWORKS" : ['dep1_frameworks'],
"FRAMEWORKPATH" : ['dep1_frameworkdirs'],
"CPPDEFINES" : ['dep1_defines'],
"CXXFLAGS" : ['dep1_cxxflags'],
"CCFLAGS" : ['dep1_cflags'],
"SHLINKFLAGS" : ['dep1_sharedlinkflags'],
"LINKFLAGS" : ['dep1_exelinkflags'],
},
"dep1_version" : "0.1",
""", """
"dep2" : {
"CPPPATH" : ['dep2_includedir'],
"LIBPATH" : ['dep2_libdir'],
"BINPATH" : ['dep2_bindir'],
"LIBS" : ['dep2_lib'],
"FRAMEWORKS" : ['dep2_frameworks'],
"FRAMEWORKPATH" : ['dep2_frameworkdirs'],
"CPPDEFINES" : ['dep2_defines'],
"CXXFLAGS" : ['dep2_cxxflags'],
"CCFLAGS" : ['dep2_cflags'],
"SHLINKFLAGS" : ['dep2_sharedlinkflags'],
"LINKFLAGS" : ['dep2_exelinkflags'],
},
"dep2_version" : "0.1",
"""]

clean_sconsdeps = clean_paths(sconsdeps)
for block in expected_content:
assert block in clean_sconsdeps