Как настроить Конана, чтобы он не связывал все возможное?

Я пробую использовать диспетчер пакетов Conan и начал писать тестовый проект на C ++, в котором используются библиотеки Poco. Я создал простую программу, которая просто расшифровывает строку с помощью AES-256-CBC. Я был очень удивлен, обнаружив, что сгенерированный двоичный файл был почти 4 МБ после сборки с помощью Conan и Cmake. Я попытался настроить файлы conanfile.txt и CmakeLists.txt, чтобы связать только необходимые библиотеки, но я либо не смог заставить проект скомпилировать, либо не смог уменьшить размер скомпилированного двоичного файла.

Я почти уверен, что PCRE, bzip2, SQLlite и другие подключаются к моему двоичному файлу, поскольку Poco зависит от них. Я просто очень смущен тем, почему gcc недостаточно умен, чтобы понять, что код Poco, который я вызываю, использует только небольшой фрагмент кода OpenSSL.

Как я могу скомпилировать / связать только то, что мне нужно, и сохранить свой двоичный файл до разумного размера?

conanfile.txt:

[requires]
poco/1.10.1

[generators]
cmake

CmakeLists.txt:

cmake_minimum_required(VERSION 3.7...3.18)

if(${CMAKE_VERSION} VERSION_LESS 3.12)
    cmake_policy(VERSION ${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION})
endif()

project(main)

add_definitions("-std=c++17")

include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake)
conan_basic_setup()

add_executable(${PROJECT_NAME} main.cpp)
target_link_libraries(${PROJECT_NAME} ${CONAN_LIBS})

main.cpp:

#include <cstdlib>
#include <iostream>
#include <sstream>

#include "Poco/Base64Decoder.h"
#include "Poco/Crypto/Cipher.h"
#include "Poco/Crypto/CipherFactory.h"
#include "Poco/Crypto/CipherKey.h"
#include "Poco/DigestStream.h"
#include "Poco/SHA2Engine.h"


std::string sha512(std::string value);
std::string aesDecrypt(const std::string ciphertext, const std::string key, const std::string iv);
std::string getEnvVar(const std::string key);
std::string base64Decode(const std::string encoded);

int main(int argc, char** argv) {
    std::string enc = "Ug7R5BQIosmn1yPeawSUIzY8N9wzASmI/w0Wz/xX7Yw=";
    std::cout << aesDecrypt(enc, "admin", "7K/OkQIrl4rqUk8/1h+uuQ==") << "\n";

    std::cout << sha512("Hello there") << "\n";
    std::cout << getEnvVar("USER") << "\n";

    return 0;
}

std::string aesDecrypt(const std::string ciphertext, const std::string key, const std::string iv) {
    auto keyHash = sha512(key);
    Poco::Crypto::Cipher::ByteVec keyBytes{keyHash.begin(), keyHash.end()};
    auto rawIV = base64Decode(iv);
    Poco::Crypto::Cipher::ByteVec ivBytes{rawIV.begin(), rawIV.end()};

    auto &factory = Poco::Crypto::CipherFactory::defaultFactory();
    auto pCipher = factory.createCipher(Poco::Crypto::CipherKey("aes-256-cbc", keyBytes, ivBytes));

    return pCipher->decryptString(ciphertext, Poco::Crypto::Cipher::ENC_BASE64);
}

std::string sha512(const std::string value) {
    Poco::SHA2Engine sha256(Poco::SHA2Engine::SHA_512);
    Poco::DigestOutputStream ds(sha256);
    ds << value;
    ds.close();

    return Poco::DigestEngine::digestToHex(sha256.digest());
}

std::string getEnvVar(const std::string key) {
    char * val = getenv(key.c_str());
    return val == NULL ? std::string("") : std::string(val);
}

std::string base64Decode(const std::string encoded) {
    std::istringstream istr(encoded);
    std::ostringstream ostr;
    Poco::Base64Decoder b64in(istr);

    copy(std::istreambuf_iterator<char>(b64in),
    std::istreambuf_iterator<char>(),
    std::ostreambuf_iterator<char>(ostr));

    return ostr.str();
}

Как я создаю код:

#!/bin/bash

set -e
set -x

rm -rf build
mkdir build
pushd build

conan install ..
cmake .. -DCMAKE_BUILD_TYPE=Release
cmake --build .

ls -lah bin/main
bin/main

person Andrew LeFevre    schedule 02.10.2020    source источник
comment
Конечно, линкеры умны, и при статической компоновке они оптимизируют и удаляют то, что не нужно. Я думаю, что он выполняет относительно хорошую работу, только библиотеки Poco имеют размер примерно 60 Мбайт, не считая транзитивных зависимостей. Только sqlite больше 2 МБ, а Openssl почти 4 МБ. Если бы они были полностью встроены в ваш исполняемый файл, ваш исполняемый файл был бы намного больше.   -  person drodri    schedule 03.10.2020
comment
Вероятно, лучше включить библиотеки, установленные с помощью conan, с использованием #include <library> вместо #include "library". См. этот вопрос.   -  person darcamo    schedule 03.10.2020


Ответы (2)


Нет ничего плохого в том, что вы делаете. Возможно, у poco много зависимостей и функций. Вы можете добавить message(STATUS "Linkd libraries: " ${CONAN_LIBS}) в CMakeLists.txt и снова запустить cmake, чтобы просмотреть библиотеки, с которыми вы в настоящее время связываетесь при использовании ${CONAN_LIBS}.

Вы также можете попробовать использовать conan_basic_setup(TARGETS) в CMakeLists.txt вместо просто conan_basic_setup(). Если вы это сделаете, то вам нужно изменить target_link_libraries(${PROJECT_NAME} ${CONAN_LIBS}) на target_link_libraries(${PROJECT_NAME} CONAN_PKG::poco). Это позволяет вам более точно контролировать, какие библиотеки в conanfile.txt вы связываете с каждой целью в CMaksLists.txt. Но поскольку ваш conanfile.txt имеет только poco в качестве зависимости, это ничего не должно изменить.

Еще одна вещь, которую вы можете попробовать, - это проверить, есть ли в poco рецепте в conan какие-либо параметры, которые вы можете установить для включения / исключения частей библиотеки poco. Выполните команду ниже (при условии, что poco/1.10.1 уже установлен в вашем кэше Conan)

conan get poco/1.10.1

Это покажет вам полный рецепт poco, который использует Конан. Вот я получил

from conans import ConanFile, CMake, tools
from conans.errors import ConanException, ConanInvalidConfiguration
from collections import namedtuple, OrderedDict
import os


class PocoConan(ConanFile):
    name = "poco"
    url = "https://github.com/conan-io/conan-center-index"
    homepage = "https://pocoproject.org"
    topics = ("conan", "poco", "building", "networking", "server", "mobile", "embedded")
    exports_sources = "CMakeLists.txt", "patches/**"
    generators = "cmake", "cmake_find_package"
    settings = "os", "arch", "compiler", "build_type"
    license = "BSL-1.0"
    description = "Modern, powerful open source C++ class libraries for building network- and internet-based " \
                  "applications that run on desktop, server, mobile and embedded systems."
    options = {
        "shared": [True, False],
        "fPIC": [True, False],
    }
    default_options = {
        "shared": False,
        "fPIC": True,
    }

    _PocoComponent = namedtuple("_PocoComponent", ("option", "default_option", "dependencies", "is_lib"))
    _poco_component_tree = {
        "mod_poco": _PocoComponent("enable_apacheconnector", False, ("PocoUtil", "PocoNet", ), False),  # also external apr and apr-util
        "PocoCppParser": _PocoComponent("enable_cppparser", False, ("PocoFoundation", ), False),
        # "PocoCppUnit": _PocoComponent("enable_cppunit", False, ("PocoFoundation", ), False)),
        "PocoCrypto": _PocoComponent("enable_crypto", True, ("PocoFoundation", ), True),    # also external openssl
        "PocoData": _PocoComponent("enable_data", True, ("PocoFoundation", ), True),
        "PocoDataMySQL": _PocoComponent("enable_data_mysql", False, ("PocoData", ), True),
        "PocoDataODBC": _PocoComponent("enable_data_odbc", False, ("PocoData", ), True),
        "PocoDataPostgreSQL": _PocoComponent("enable_data_postgresql", False, ("PocoData", ), True),    # also external postgresql
        "PocoDataSQLite": _PocoComponent("enable_data_sqlite", True, ("PocoData", ), True),  # also external sqlite3
        "PocoEncodings": _PocoComponent("enable_encodings", True, ("PocoFoundation", ), True),
        # "PocoEncodingsCompiler": _PocoComponent("enable_encodingscompiler", False, ("PocoNet", "PocoUtil", ), False),
        "PocoFoundation": _PocoComponent(None, "PocoFoundation", (), True),
        "PocoJSON": _PocoComponent("enable_json", True, ("PocoFoundation", ), True),
        "PocoJWT": _PocoComponent("enable_jwt", True, ("PocoJSON", "PocoCrypto", ), True),
        "PocoMongoDB": _PocoComponent("enable_mongodb", True, ("PocoNet", ), True),
        "PocoNet": _PocoComponent("enable_net", True, ("PocoFoundation", ), True),
        "PocoNetSSL": _PocoComponent("enable_netssl", True, ("PocoCrypto", "PocoUtil", "PocoNet", ), True),    # also external openssl
        "PocoNetSSLWin": _PocoComponent("enable_netssl_win", True, ("PocoNet", "PocoUtil", ), True),
        "PocoPDF": _PocoComponent("enable_pdf", False, ("PocoXML", "PocoUtil", ), True),
        "PocoPageCompiler": _PocoComponent("enable_pagecompiler", False, ("PocoNet", "PocoUtil", ), False),
        "PocoFile2Page": _PocoComponent("enable_pagecompiler_file2page", False, ("PocoNet", "PocoUtil", "PocoXML", "PocoJSON", ), False),
        "PocoPocoDoc": _PocoComponent("enable_pocodoc", False, ("PocoUtil", "PocoXML", "PocoCppParser", ), False),
        "PocoRedis": _PocoComponent("enable_redis", True, ("PocoNet", ), True),
        "PocoSevenZip": _PocoComponent("enable_sevenzip", False, ("PocoUtil", "PocoXML", ), True),
        "PocoUtil": _PocoComponent("enable_util", True, ("PocoFoundation", "PocoXML", "PocoJSON", ), True),
        "PocoXML": _PocoComponent("enable_xml", True, ("PocoFoundation", ), True),
        "PocoZip": _PocoComponent("enable_zip", True, ("PocoUtil", "PocoXML", ), True),
    }
    
    for comp in _poco_component_tree.values():
        if comp.option:
            options[comp.option] = [True, False]
            default_options[comp.option] = comp.default_option
    del comp

    @property
    def _poco_ordered_components(self):
        remaining_components = dict((compname, set(compopts.dependencies)) for compname, compopts in self._poco_component_tree.items())
        ordered_components = []
        while remaining_components:
            components_no_deps = set(compname for compname, compopts in remaining_components.items() if not compopts)
            if not components_no_deps:
                raise ConanException("The poco dependency tree is invalid and contains a cycle")
            for c in components_no_deps:
                remaining_components.pop(c)
            ordered_components.extend(components_no_deps)
            for rname in remaining_components.keys():
                remaining_components[rname] = remaining_components[rname].difference(components_no_deps)
        ordered_components.reverse()
        return ordered_components

    _cmake = None

    @property
    def _source_subfolder(self):
        return "source_subfolder"

    @property
    def _build_subfolder(self):
        return "build_subfolder"

    def source(self):
        tools.get(**self.conan_data["sources"][self.version])
        extracted_folder = "poco-poco-{}-release".format(self.version)
        os.rename(extracted_folder, self._source_subfolder)

    def config_options(self):
        if self.settings.os == "Windows":
            del self.options.fPIC
        else:
            del self.options.enable_netssl_win
        if tools.Version(self.version) < "1.9":
            del self.options.enable_encodings
        if tools.Version(self.version) < "1.10":
            del self.options.enable_data_postgresql
            del self.options.enable_jwt

    def configure(self):
        if self.options.enable_apacheconnector:
            raise ConanInvalidConfiguration("Apache connector not supported: https://github.com/pocoproject/poco/issues/1764")
        if self.options.enable_data_mysql:
            raise ConanInvalidConfiguration("MySQL not supported yet, open an issue here please: %s" % self.url)
        if self.options.get_safe("enable_data_postgresql", False):
            raise ConanInvalidConfiguration("PostgreSQL not supported yet, open an issue here please: %s" % self.url)
        for compopt in self._poco_component_tree.values():
            if not compopt.option:
                continue
            if self.options.get_safe(compopt.option, False):
                for compdep in compopt.dependencies:
                    if not self._poco_component_tree[compdep].option:
                        continue
                    if not self.options.get_safe(self._poco_component_tree[compdep].option, False):
                        raise ConanInvalidConfiguration("option {} requires also option {}".format(compopt.option, self._poco_component_tree[compdep].option))

    def requirements(self):
        self.requires("pcre/8.41")
        self.requires("zlib/1.2.11")
        if self.options.enable_xml:
            self.requires("expat/2.2.9")
        if self.options.enable_data_sqlite:
            self.requires("sqlite3/3.31.1")
        if self.options.enable_apacheconnector:
            self.requires("apr/1.7.0")
            self.requires("apr-util/1.6.1")
            raise ConanInvalidConfiguration("apache2 is not (yet) available on CCI")
            self.requires("apache2/x.y.z")
        if self.options.enable_netssl or \
                self.options.enable_crypto or \
                self.options.get_safe("enable_jwt", False):
            self.requires("openssl/1.1.1g")

    def _patch_sources(self):
        for patch in self.conan_data.get("patches", {}).get(self.version, []):
            tools.patch(**patch)

    def _configure_cmake(self):
        if self._cmake:
            return self._cmake
        self._cmake = CMake(self)
        if tools.Version(self.version) < "1.10.1":
            self._cmake.definitions["POCO_STATIC"] = not self.options.shared
        for comp in self._poco_component_tree.values():
            if not comp.option:
                continue
            self._cmake.definitions[comp.option.upper()] = self.options.get_safe(comp.option, False)
        self._cmake.definitions["POCO_UNBUNDLED"] = True
        self._cmake.definitions["CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS_SKIP"] = True
        if self.settings.os == "Windows" and self.settings.compiler == "Visual Studio":  # MT or MTd
            self._cmake.definitions["POCO_MT"] = "ON" if "MT" in str(self.settings.compiler.runtime) else "OFF"
        self.output.info(self._cmake.definitions)
        # On Windows, Poco needs a message (MC) compiler.
        with tools.vcvars(self.settings) if self.settings.compiler == "Visual Studio" else tools.no_op():
            self._cmake.configure(build_dir=self._build_subfolder)
        return self._cmake

    def build(self):
        if self.options.enable_data_sqlite:
            if self.options["sqlite3"].threadsafe == 0:
                raise ConanInvalidConfiguration("sqlite3 must be built with threadsafe enabled")
        self._patch_sources()
        cmake = self._configure_cmake()
        cmake.build()

    def package(self):
        self.copy("LICENSE", dst="licenses", src=self._source_subfolder)
        cmake = self._configure_cmake()
        cmake.install()
        tools.rmdir(os.path.join(self.package_folder, "lib", "cmake"))
        tools.rmdir(os.path.join(self.package_folder, "cmake"))

    @property
    def _ordered_libs(self):
        libs = []
        for compname in self._poco_ordered_components:
            comp_options = self._poco_component_tree[compname]
            if comp_options.is_lib:
                if not comp_options.option:
                    libs.append(compname)
                elif self.options.get_safe(comp_options.option, False):
                    libs.append(compname)
        return libs

    def package_info(self):
        suffix = str(self.settings.compiler.runtime).lower()  \
                 if self.settings.compiler == "Visual Studio" and not self.options.shared \
                 else ("d" if self.settings.build_type == "Debug" else "")

        self.cpp_info.libs = list("{}{}".format(lib, suffix) for lib in self._ordered_libs)
        
        if self.settings.os == "Linux":
            self.cpp_info.system_libs.extend(["pthread", "dl", "rt"])

        if self.settings.compiler == "Visual Studio":
            self.cpp_info.defines.append("POCO_NO_AUTOMATIC_LIBS")
        if not self.options.shared:
            self.cpp_info.defines.append("POCO_STATIC=ON")
            if self.settings.compiler == "Visual Studio":
                self.cpp_info.system_libs.extend(["ws2_32", "iphlpapi", "crypt32"])
        self.cpp_info.names["cmake_find_package"] = "Poco"
        self.cpp_info.names["cmake_find_package_multi"] = "Poco"

Вы хотите увидеть, что в рецепте есть в options и default_options. Насколько мне известно, нет никакого способа узнать, какие опции предоставляет рецепт и что они делают, кроме как смотреть на реальный исходный код рецепта, подобный этому.

Кажется, что рецепт poco добавляет много вариантов из этого _poco_component_tree словаря. Вы хотите проверить параметры с именами enable_something и значением True. Поскольку они добавляются как опции, это означает, что клиент (вы используете conan) может управлять ими при запуске conan install. Например, попробуйте команду ниже (вы можете добавить несколько -o poco:something, чтобы установить несколько параметров)

conan install .. -o poco:enable_data_sqlite=False

Мы можем видеть в методе requirements в рецепте, что только когда enable_data_sqlite равно True, conan добавит sqlite3 / 3.31.1 является зависимостью от poco. Это означает, что если вы установите enable_data_sqlite на False, то его вообще не следует включать, а ваш двоичный файл должен стать меньше.

Поскольку conan (и разработчики poco, или кто-либо другой, кто создал рецепт poco) хочет максимально упростить установку poco с помощью conan, имеет смысл по умолчанию включить наиболее распространенные части poco. Вы можете контролировать это с помощью параметров Conan для отключения его частей. Вам придется попробовать несколько из этих вариантов, чтобы увидеть, что у вас получится. Если вы отключите что-то, что вам действительно нужно, вы получите ошибки при компиляции и / или компоновке вашего фактического кода.

person darcamo    schedule 02.10.2020

Внимательно посмотрите на Poco/Config.h, так как там есть несколько макросов, которые позволяют отключать определенные части Poco. Эти макросы существуют, чтобы помочь вам легко разделить ваши двоичные файлы, если они вам не нужны (например, файлы конфигурации XML, JSON, INI, также POCO_NO_AUTOMATIC_LIBS). Я ожидал, что они и другие уменьшат размер вашего объектного файла.

Я знаю, что это было довольно давно, но Poco использовался для запуска веб-сервера на очень маленькой плате. См. https://pocoproject.org/blog/?p=193.

person Cookie Butter    schedule 04.10.2020