Skip to content
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
30 changes: 23 additions & 7 deletions src/server/compiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,13 @@ void Compiler::init_compile_graph() {
// Lazy dependency resolver: scans a module file on demand to discover imports.
auto resolve = [this](std::uint32_t path_id) -> llvm::SmallVector<std::uint32_t> {
auto file_path = workspace.path_pool.resolve(path_id);
auto results =
workspace.cdb.lookup(file_path, {.query_toolchain = true, .suppress_logging = true});
std::vector<std::string> rule_append, rule_remove;
workspace.config.match_rules(file_path, rule_append, rule_remove);
auto results = workspace.cdb.lookup(file_path,
{.query_toolchain = true,
.suppress_logging = true,
.remove = rule_remove,
.append = rule_append});
if(results.empty())
return {};

Expand Down Expand Up @@ -97,7 +102,8 @@ void Compiler::init_compile_graph() {
}
auto args_hash = llvm::xxh3_64bits(llvm::StringRef(hash_input));
auto pcm_filename = std::format("{}-{:016x}.pcm", safe_module_name, args_hash);
auto pcm_path = path::join(workspace.config.cache_dir, "cache", "pcm", pcm_filename);
auto pcm_path =
path::join(workspace.config.project.cache_dir, "cache", "pcm", pcm_filename);

// Check if cached PCM is still valid.
if(auto pcm_it = workspace.pcm_cache.find(path_id); pcm_it != workspace.pcm_cache.end()) {
Expand Down Expand Up @@ -156,7 +162,11 @@ bool Compiler::fill_compile_args(llvm::StringRef path,
}

// 2. Normal CDB lookup for the file itself.
auto results = workspace.cdb.lookup(path, {.query_toolchain = true});
// Apply rules from config (append/remove flags based on file patterns).
std::vector<std::string> rule_append, rule_remove;
workspace.config.match_rules(path, rule_append, rule_remove);
CommandOptions opts{.query_toolchain = true, .remove = rule_remove, .append = rule_append};
auto results = workspace.cdb.lookup(path, opts);
if(!results.empty()) {
auto& cmd = results.front();
directory = cmd.resolved.directory.str();
Expand Down Expand Up @@ -205,7 +215,13 @@ bool Compiler::fill_header_context_args(llvm::StringRef path,
}

auto host_path = workspace.path_pool.resolve(ctx_ptr->host_path_id);
auto host_results = workspace.cdb.lookup(host_path, {.query_toolchain = true});
// Apply rules matching the HEADER path (what the user is editing) on top of
// the host's command — rules are expected to apply uniformly to every file.
std::vector<std::string> rule_append, rule_remove;
workspace.config.match_rules(path, rule_append, rule_remove);
auto host_results = workspace.cdb.lookup(
host_path,
{.query_toolchain = true, .remove = rule_remove, .append = rule_append});
if(host_results.empty()) {
LOG_WARN("fill_header_context_args: host {} has no CDB entry", host_path);
return false;
Expand Down Expand Up @@ -355,7 +371,7 @@ std::optional<HeaderFileContext> Compiler::resolve_header_context(std::uint32_t
// Hash the preamble and write to cache directory.
auto preamble_hash = llvm::xxh3_64bits(llvm::StringRef(preamble));
auto preamble_filename = std::format("{:016x}.h", preamble_hash);
auto preamble_dir = path::join(workspace.config.cache_dir, "header_context");
auto preamble_dir = path::join(workspace.config.project.cache_dir, "header_context");
auto preamble_path = path::join(preamble_dir, preamble_filename);

if(!llvm::sys::fs::exists(preamble_path)) {
Expand Down Expand Up @@ -438,7 +454,7 @@ kota::task<bool> Compiler::ensure_pch(Session& session,
auto preamble_hash = llvm::xxh3_64bits(preamble_text);

// Deterministic content-addressed PCH path.
auto pch_path = path::join(workspace.config.cache_dir,
auto pch_path = path::join(workspace.config.project.cache_dir,
"cache",
"pch",
std::format("{:016x}.pch", preamble_hash));
Expand Down
186 changes: 139 additions & 47 deletions src/server/config.cpp
Original file line number Diff line number Diff line change
@@ -1,99 +1,191 @@
#include "server/config.h"

#include <algorithm>
#include <thread>

#include "support/filesystem.h"
#include "support/glob_pattern.h"
#include "support/logging.h"

#include "kota/codec/json/json.h"
#include "kota/codec/toml.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/Process.h"
#include "llvm/Support/xxhash.h"

namespace clice {

/// Replace all occurrences of ${workspace} with the workspace root.
static void substitute_workspace(std::string& value, const std::string& workspace_root) {
/// No-op when workspace_root is empty, to avoid producing paths like "/cache"
/// from "${workspace}/cache".
static void substitute_workspace(std::string& value, llvm::StringRef workspace_root) {
if(workspace_root.empty())
return;
constexpr std::string_view placeholder = "${workspace}";
std::string::size_type pos = 0;
std::size_t pos = 0;
while((pos = value.find(placeholder, pos)) != std::string::npos) {
value.replace(pos, placeholder.size(), workspace_root);
pos += workspace_root.size();
}
}

void CliceConfig::apply_defaults(const std::string& workspace_root) {
auto cpu_count = std::thread::hardware_concurrency();
if(cpu_count == 0)
cpu_count = 4;

if(stateful_worker_count == 0) {
stateful_worker_count = 2;
}
if(stateless_worker_count == 0) {
stateless_worker_count = 3;
}
if(worker_memory_limit == 0) {
worker_memory_limit = 4ULL * 1024 * 1024 * 1024; // 4GB default
}
if(cache_dir.empty() && !workspace_root.empty()) {
cache_dir = path::join(workspace_root, ".clice");
/// Try to resolve the default cache directory using XDG_CACHE_HOME.
/// Returns empty string on failure.
static std::string resolve_xdg_cache_dir(llvm::StringRef workspace_root) {
// Determine base: $XDG_CACHE_HOME or ~/.cache
std::string base;
if(auto xdg = llvm::sys::Process::GetEnv("XDG_CACHE_HOME"); xdg && !xdg->empty()) {
base = std::move(*xdg);
} else if(auto home = llvm::sys::Process::GetEnv("HOME"); home && !home->empty()) {
base = path::join(*home, ".cache");
} else {
return {};
}

if(index_dir.empty() && !cache_dir.empty()) {
index_dir = path::join(cache_dir, "index");
// Use a hash of workspace_root to create a unique subdirectory.
auto hash = llvm::xxh3_64bits(workspace_root);
auto dir = path::join(base, "clice", std::format("{:016x}", hash));

if(auto ec = llvm::sys::fs::create_directories(dir)) {
LOG_WARN("Failed to create XDG cache directory {}: {}", dir, ec.message());
return {};
}
return dir;
}

if(logging_dir.empty() && !cache_dir.empty()) {
logging_dir = path::join(cache_dir, "logs");
void Config::apply_defaults(llvm::StringRef workspace_root) {
auto& p = project;

if(p.max_active_file == 0)
p.max_active_file = 8;
if(!p.enable_indexing)
p.enable_indexing = true;
if(!p.idle_timeout_ms)
p.idle_timeout_ms = 3000;

if(p.stateful_worker_count == 0)
p.stateful_worker_count = 2;
if(p.stateless_worker_count == 0)
p.stateless_worker_count = 3;
if(p.worker_memory_limit == 0)
p.worker_memory_limit = 4ULL * 1024 * 1024 * 1024; // 4GB

if(p.cache_dir.empty() && !workspace_root.empty()) {
p.cache_dir = resolve_xdg_cache_dir(workspace_root);
if(p.cache_dir.empty())
p.cache_dir = path::join(workspace_root, ".clice");
}
if(p.index_dir.empty() && !p.cache_dir.empty())
p.index_dir = path::join(p.cache_dir, "index");
if(p.logging_dir.empty() && !p.cache_dir.empty())
p.logging_dir = path::join(p.cache_dir, "logs");

// Variable substitution on string fields.
substitute_workspace(p.cache_dir, workspace_root);
substitute_workspace(p.index_dir, workspace_root);
substitute_workspace(p.logging_dir, workspace_root);
for(auto& entry: p.compile_commands_paths)
substitute_workspace(entry, workspace_root);

// Pre-compile glob patterns from rules.
compiled_rules.clear();
for(auto& rule: rules) {
CompiledRule compiled;
for(auto& pattern_str: rule.patterns) {
auto pat = GlobPattern::create(pattern_str);
if(!pat) {
LOG_WARN("Invalid glob pattern in rule: {}", pattern_str);
continue;
}
compiled.patterns.push_back(std::move(*pat));
}
// Drop the whole rule if no pattern compiled successfully — otherwise the
// append/remove flags would be silently attached to a rule that can never match.
if(compiled.patterns.empty()) {
if(!rule.patterns.empty())
LOG_WARN("Rule dropped: all glob patterns failed to compile");
continue;
}
compiled.append.assign(rule.append.begin(), rule.append.end());
compiled.remove.assign(rule.remove.begin(), rule.remove.end());
compiled_rules.push_back(std::move(compiled));
}
}

// Apply variable substitution to string fields
substitute_workspace(compile_commands_path, workspace_root);
substitute_workspace(cache_dir, workspace_root);
substitute_workspace(index_dir, workspace_root);
substitute_workspace(logging_dir, workspace_root);
void Config::match_rules(llvm::StringRef file_path,
std::vector<std::string>& append,
std::vector<std::string>& remove) const {
// Rules are processed in declaration order so that a later rule can
// override an earlier one. Specifically, when a later rule removes
// an argument, we also strip any string-equal entry already added
// to `append` by an earlier matching rule — otherwise the append
// would silently survive (lookup applies removes to the base flags
// only, not to entries contributed via `append`).
for(auto& rule: compiled_rules) {
bool matched =
std::ranges::any_of(rule.patterns, [&](auto& pat) { return pat.match(file_path); });
if(!matched)
continue;

for(auto& r: rule.remove) {
std::erase(append, r);
remove.push_back(r);
}
append.insert(append.end(), rule.append.begin(), rule.append.end());
}
}

std::optional<CliceConfig> CliceConfig::load(const std::string& path,
const std::string& workspace_root) {
std::optional<Config> Config::load(llvm::StringRef path, llvm::StringRef workspace_root) {
auto content = fs::read(path);
if(!content) {
if(!content)
return std::nullopt;
}

auto result = kota::codec::toml::parse<CliceConfig>(*content);
auto result = kota::codec::toml::parse<Config>(*content);
if(!result) {
LOG_WARN("Failed to parse config file {}", path);
LOG_ERROR("Invalid clice.toml {}: {}", path, result.error().to_string());
return std::nullopt;
}

auto config = std::move(*result);
config.apply_defaults(workspace_root);

LOG_INFO("Loaded config from {}", path);
return config;
}

CliceConfig CliceConfig::load_from_workspace(const std::string& workspace_root) {
std::optional<Config> Config::load_from_json(llvm::StringRef json, llvm::StringRef workspace_root) {
auto result = kota::codec::json::from_json<Config>(json);
if(!result) {
LOG_WARN("Failed to parse initializationOptions JSON: {}", result.error().message());
return std::nullopt;
}

auto config = std::move(*result);
config.apply_defaults(workspace_root);
LOG_INFO("Loaded config from initializationOptions");
return config;
}

Config Config::load_from_workspace(llvm::StringRef workspace_root) {
if(!workspace_root.empty()) {
// Try standard config file locations
for(auto* name: {"clice.toml", ".clice/config.toml"}) {
auto config_path = path::join(workspace_root, name);
if(llvm::sys::fs::exists(config_path)) {
auto config = load(config_path, workspace_root);
if(config)
return std::move(*config);
}
if(!llvm::sys::fs::exists(config_path))
continue;
if(auto config = load(config_path, workspace_root))
return std::move(*config);
// Present but malformed: fall through to defaults, but surface
// the situation clearly so users know their config wasn't applied.
LOG_WARN("Falling back to default configuration because {} is invalid", config_path);
}
}

// No config file found; use defaults
CliceConfig config;
Config config;
config.apply_defaults(workspace_root);
LOG_INFO(
"No clice.toml found, using default configuration " "(stateful={}, stateless={}, memory_limit={}MB)",
config.stateful_worker_count,
config.stateless_worker_count,
config.worker_memory_limit / (1024 * 1024));
config.project.stateful_worker_count.value,
config.project.stateless_worker_count.value,
config.project.worker_memory_limit.value / (1024 * 1024));
return config;
}

Expand Down
Loading
Loading