Skip to content

Commit

Permalink
Fall back to PC file when CPS is not available
Browse files Browse the repository at this point in the history
This hooks PC file parsing into existing logic by translating PC file
content into a "bad" CPS file. To a limited extent, this enables
cps-config to replicate pkg-config behavior when operating on pc files.

This implementation has the following known
issues/omissions/imperfections:
1. Cflags and Libs are taken as-is.
2. Library location is assumed to be a possibly non-existent
`@prefix@/lib/{name}.a`.
3. Requires are not parsed and not respected.
4. Libs.private and Cflags.private is not respected.
  • Loading branch information
lunacd authored and dcbaker committed Feb 11, 2025
1 parent 5dd491c commit 50a3d6c
Show file tree
Hide file tree
Showing 15 changed files with 190 additions and 51 deletions.
3 changes: 3 additions & 0 deletions src/cps/env.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ namespace cps {
// TODO: Windows
env.cps_prefix_path = utils::split(env_c, ":");
}
if (const char * env_c = std::getenv("PKG_CONFIG_PATH")) {
env.pc_path = std::string(env_c);
}
if (std::getenv("PKG_CONFIG_DEBUG_SPEW") || std::getenv("CPS_CONFIG_DEBUG_SPEW")) {
env.debug_spew = true;
}
Expand Down
1 change: 1 addition & 0 deletions src/cps/env.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ namespace cps {
struct Env {
std::optional<std::string> cps_path = std::nullopt;
std::optional<std::vector<std::string>> cps_prefix_path = std::nullopt;
std::optional<std::string> pc_path = std::nullopt;
bool debug_spew = false;
};

Expand Down
2 changes: 0 additions & 2 deletions src/cps/loader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ namespace cps::loader {

namespace {

constexpr static std::string_view CPS_VERSION = "0.12.0";

template <typename T>
tl::expected<std::optional<T>, std::string>
get_optional(const nlohmann::json & parent, std::string_view parent_name, const std::string & name) {
Expand Down
2 changes: 2 additions & 0 deletions src/cps/loader.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ namespace cps::loader {
version::Schema version_schema;
};

constexpr inline std::string_view CPS_VERSION = "0.12.0";

tl::expected<Package, std::string> load(std::istream & input_buffer, const std::filesystem::path & filename);

} // namespace cps::loader
9 changes: 7 additions & 2 deletions src/cps/pc_compat/pc.l
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ comment #[^\n]*
<<EOF>> { return yy::parser::make_YYEOF(); }
%%

void PcLoader::scan_begin(std::istream &istream) const {
yyinput = &istream;
namespace cps::pc_compat {

void PcLoader::scan_begin(std::istream &istream) const {
yyinput = &istream;
}

}

6 changes: 4 additions & 2 deletions src/cps/pc_compat/pc.y
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
#include <string>
#include <istream>
#include "cps/utils.hpp"
class PcLoader;
namespace cps::pc_compat {
class PcLoader;
}
}

// The parsing context.
%param { PcLoader& loader }
%param { cps::pc_compat::PcLoader& loader }

%define parse.trace
%define parse.error detailed
Expand Down
88 changes: 79 additions & 9 deletions src/cps/pc_compat/pc_loader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,87 @@

#include "cps/pc_compat/pc_loader.hpp"

#include "cps/error.hpp"
#include "cps/pc_compat/pc.parser.hpp"
#include "cps/utils.hpp"
#include "cps/version.hpp"

PcLoader::PcLoader() = default;
#include <fmt/format.h>

void PcLoader::load(std::istream & istream) {
scan_begin(istream);
yy::parser parse(*this);
// To debug parser, uncomment the following line
// TODO: add a way to enable debug output without rebuilding
namespace cps::pc_compat {

PcLoader::PcLoader() = default;

tl::expected<loader::Package, std::string> PcLoader::load(std::istream & istream, std::string const & filename) {
scan_begin(istream);
yy::parser parse(*this);
// To debug parser, uncomment the following line
// TODO: add a way to enable debug output without rebuilding
// parse.set_debug_level(true);
if (const int result = parse(); result != 0) {
throw std::runtime_error("Failed to parse the given pkg-config file.");
if (const int result = parse(); result != 0) {
throw std::runtime_error("Failed to parse the given pkg-config file.");
}

const auto get_property = [this](const std::string & property_name) -> tl::expected<std::string, std::string> {
const auto it = properties.find(property_name);
if (it == properties.end()) {
return tl::make_unexpected(fmt::format("Pkg-config property {} is not defined.", property_name));
}
return it->second;
};

std::string name = CPS_TRY(get_property("Name"));
loader::LangValues compile_flags;
if (auto compile_flags_input = get_property("Cflags")) {
auto compile_flags_vec = utils::split(*compile_flags_input);
compile_flags.emplace(loader::KnownLanguages::c, compile_flags_vec);
compile_flags.emplace(loader::KnownLanguages::cxx, compile_flags_vec);
compile_flags.emplace(loader::KnownLanguages::fortran, compile_flags_vec);
}
std::vector<std::string> link_flags;
if (auto link_flags_input = get_property("Libs")) {
link_flags = cps::utils::split(*link_flags_input);
}
std::unordered_map<std::string, loader::Component> components;
components.emplace(name, loader::Component{
.type = loader::Type::unknown,
.compile_flags = compile_flags,
.includes = loader::LangValues{},
.definitions = loader::Defines{},
.link_flags = link_flags,
.link_libraries = {},
// TODO: Currently lib location is hard coded to appease assertions. This would
// need to implement linker-like search to replicate current behavior.
.location = fmt::format("@prefix@/lib/{}.a", name),
.link_location = std::nullopt,
.require = {} // TODO: Parse requires
});

const auto version = CPS_TRY(get_property("Version").and_then(get_string));

return loader::Package{.name = name,
.cps_version = std::string{loader::CPS_VERSION},
.components = components,
// TODO: treat PREFIX in pc file specially and translate it to @prefix@
.cps_path = std::nullopt,
.filename = filename,
.default_components = std::vector{name},
.platform = std::nullopt,
.require = {}, // TODO: Parse requires
.version = version,
.version_schema = version::Schema::custom};
}
}

tl::expected<std::string, std::string> PcLoader::get_property(const std::string & property_name) const {
if (const auto it = properties.find(property_name); it != properties.end()) {
return it->second;
}
return tl::make_unexpected(fmt::format("Property {} is not specified", property_name));
}

tl::expected<loader::Package, std::string> load(std::istream & istream, std::string const & filename) {
PcLoader loader;
return loader.load(istream, filename);
}

} // namespace cps::pc_compat
40 changes: 27 additions & 13 deletions src/cps/pc_compat/pc_loader.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,39 @@
#pragma once

#include "cps/pc_compat/pc.parser.hpp"

#include "cps/loader.hpp"

#include <tl/expected.hpp>
#include <unordered_map>

class PcLoader {
public:
PcLoader();
namespace cps::pc_compat {

class PcLoader {
public:
PcLoader();

// Properties set by pc files
// For example, Name: libfoo
std::unordered_map<std::string, std::string> properties;

// Variables defined by pc files
// For example, libdir=${PREFIX}/lib
std::unordered_map<std::string, std::string> variables;

tl::expected<loader::Package, std::string> load(std::istream & istream, std::string const & filename);

// Properties set by pc files
// For example, Name: libfoo
std::unordered_map<std::string, std::string> properties;
void scan_begin(std::istream & istream) const;

// Variables defined by pc files
// For example, libdir=${PREFIX}/lib
std::unordered_map<std::string, std::string> variables;
private:
tl::expected<std::string, std::string> get_property(const std::string & property_name) const;
};

void load(std::istream & istream);
// Free function to keep a consistent interface with `cps::loader::load`
tl::expected<loader::Package, std::string> load(std::istream & istream, std::string const & filename);

void scan_begin(std::istream & istream) const;
};
} // namespace cps::pc_compat

// Marking maybe_unused because scanner does not user this parameter
#define YY_DECL yy::parser::symbol_type yylex([[maybe_unused]] PcLoader & loader)
#define YY_DECL yy::parser::symbol_type yylex([[maybe_unused]] cps::pc_compat::PcLoader & loader)
YY_DECL;
62 changes: 45 additions & 17 deletions src/cps/search.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

#include "cps/error.hpp"
#include "cps/loader.hpp"
#include "cps/pc_compat/pc_loader.hpp"
#include "cps/platform.hpp"
#include "cps/utils.hpp"
#include "cps/version.hpp"
Expand Down Expand Up @@ -80,7 +81,14 @@ namespace cps::search {
// TODO: const std::vector<std::string> mac_prefix{""};
// TODO: const std::vector<std::string> win_prefix{""};

std::vector<fs::path> cached_paths{};
enum class SearchPathType { cps, pc };

struct SearchPath {
fs::path path;
SearchPathType type;
};

std::vector<SearchPath> cached_paths{};

/// @brief expands a single search prefix into a set of full paths
/// @param prefix the prefix to build from
Expand All @@ -98,38 +106,60 @@ namespace cps::search {
return paths;
};

template <typename T> void add_to_search_path(const std::vector<T> & paths, SearchPathType type) {
std::transform(paths.begin(), paths.end(), std::back_inserter(cached_paths),
[&type](const auto & path) { return SearchPath{.path = path, .type = type}; });
};

/// @brief Expands CPS search prefixes into concrete paths
/// @param env stored environment variables
/// @return A vector of paths to search, in order
const std::vector<fs::path> search_paths(const Env & env) {
std::vector<SearchPath> search_paths(const Env & env) {

if (!cached_paths.empty()) {
return cached_paths;
}

if (env.cps_path) {
auto && paths = utils::split(env.cps_path.value());
cached_paths.reserve(paths.size());
cached_paths.insert(cached_paths.end(), paths.begin(), paths.end());
add_to_search_path(paths, SearchPathType::cps);
}

if (env.cps_prefix_path) {
auto && prefixes = env.cps_prefix_path.value();
for (auto && p : prefixes) {
auto && paths = expand_prefix(p);
cached_paths.reserve(cached_paths.size() + paths.size());
cached_paths.insert(cached_paths.end(), paths.begin(), paths.end());
add_to_search_path(paths, SearchPathType::cps);
}
}

// If PKG_CONFIG_PATH is defined, search for PC files in the specified directly before falling back to
// system default CPS and PC search paths.
if (env.pc_path) {
cached_paths.emplace_back(SearchPath{.path = *env.pc_path, .type = SearchPathType::pc});
}

for (auto && p : nix_prefix) {
auto && paths = expand_prefix(p);
cached_paths.reserve(cached_paths.size() + paths.size());
cached_paths.insert(cached_paths.end(), paths.begin(), paths.end());
add_to_search_path(paths, SearchPathType::cps);
add_to_search_path(paths, SearchPathType::pc);
}

return cached_paths;
}

std::optional<fs::path> find_library_in_path(std::string_view name, const SearchPath & search_path) {
if (fs::is_directory(search_path.path)) {
const std::string extension = search_path.type == SearchPathType::cps ? "cps" : "pc";
// TODO: <name-like>
if (fs::path file = search_path.path / fmt::format("{}.{}", name, extension);
fs::is_regular_file(file)) {
return file;
}
}
return std::nullopt;
}

/// @brief Find all possible paths for a given CPS name
/// @param name The name of the CPS file to find
/// @return A vector of paths which patch the given name, or an error
Expand All @@ -146,13 +176,9 @@ namespace cps::search {
// dependency?
auto && paths = search_paths(env);
std::vector<fs::path> found{};
for (auto && path : paths) {
if (fs::is_directory(path)) {
// TODO: <name-like>
const fs::path file = path / fmt::format("{}.cps", name);
if (fs::is_regular_file(file)) {
found.push_back(file);
}
for (auto && search_path : paths) {
if (auto file = find_library_in_path(name, search_path)) {
found.push_back(*file);
}
}

Expand Down Expand Up @@ -212,7 +238,10 @@ namespace cps::search {

std::ifstream file;
file.open(path);
auto n = std::make_shared<Node>(CPS_TRY(loader::load(file, path)));

// Assume file is CPS unless file extension is .pc
auto n = std::make_shared<Node>(CPS_TRY(
path.extension() == ".pc" ? pc_compat::load(file, path.parent_path()) : loader::load(file, path)));

cache.emplace(name, n);
return n;
Expand All @@ -227,7 +256,6 @@ namespace cps::search {
const std::vector<fs::path> paths = CPS_TRY(find_paths(name, env));
std::vector<std::string> errors{};
for (auto && path : paths) {

auto maybe_node = factory.get(name, path);
if (!maybe_node) {
errors.emplace_back(
Expand Down
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ foreach (test_name test_case IN ZIP_LISTS test_names test_cases)
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
COMMAND
${CMAKE_COMMAND} -E env CPS_PREFIX_PATH=${CMAKE_CURRENT_SOURCE_DIR}/cps-files
PKG_CONFIG_PATH=${CMAKE_CURRENT_SOURCE_DIR}/cps-files/lib/pkgconfig
${Python_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/runner.py
$<TARGET_FILE:cps-config> ${test_case} --libdir "${CMAKE_INSTALL_LIBDIR}"
)
Expand Down
6 changes: 6 additions & 0 deletions tests/cases/pkg-config-compat.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,9 @@ args = ["pkg-config", "--modversion", "--print-errors", "--errors-to-stdout"]
expected = "Tried /.*/cps/multiple-components.cps, which does not specify a version, but the user requires version 1.0"
returncode = 1
re = true

[[case]]
name = "parsing pc file"
cps = "pc-variables"
args = ["pkg-config", "--cflags"]
expected = "-I/home/kaniini/pkg/include/libfoo"
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading

0 comments on commit 50a3d6c

Please sign in to comment.