Merge branch 'claude/forward-project-args': sibling projects inherit the args, lint with the lite rules
PR #40. GitProject and LocalProject default their args to the loading project's own (ProjectArgs()), this tree lints with LintRules' lite set, and the fixes from the branch's review: a project file gets its args only for the CrafterBuildProject call, host tooling gets project cache entries of its own, and `--` ends crafter-build's options. The conflicts with claude/dependency-notes are all places both sides added to: - Platform.cppm and Platform.cpp: PragmaDependencies() beside ProjectArgs() and LoadingHostTooling(), all three kept. - LoadProject: the record PragmaDependencies() reads moves up with the frame's dependencies, which this branch sets before dlopen. Left where it was, it would have copied preDeps.configs after the vector was moved into the frame, and recorded nothing. - README: the pragma section's new last sentence goes before the new "Dependency notes for agents" section. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
75142276ab
23 changed files with 599 additions and 62 deletions
|
|
@ -413,6 +413,11 @@ namespace {
|
|||
|
||||
struct ProjectFileFrame {
|
||||
fs::path project;
|
||||
// What LoadProject was given, for ProjectArgs(), and only once
|
||||
// CrafterBuildProject is about to be called (see LoadProject). A view
|
||||
// is enough: the caller's storage outlives LoadProject, and the frame
|
||||
// is popped before LoadProject returns.
|
||||
std::span<const std::string_view> args;
|
||||
std::vector<Configuration*> dependencies;
|
||||
};
|
||||
|
||||
|
|
@ -441,7 +446,7 @@ namespace {
|
|||
throw std::runtime_error(std::format("circular project dependency: {}{}", chain, absProject.string()));
|
||||
}
|
||||
index_ = LoadingFrames.size();
|
||||
LoadingFrames.push_back({ absProject, {} });
|
||||
LoadingFrames.push_back({ absProject, {}, {} });
|
||||
}
|
||||
~ProjectFileGuard() { LoadingFrames.pop_back(); }
|
||||
ProjectFileGuard(const ProjectFileGuard&) = delete;
|
||||
|
|
@ -453,13 +458,29 @@ namespace {
|
|||
std::size_t index_ = 0;
|
||||
};
|
||||
|
||||
// How many `#pragma crafter dependency` loads this thread is inside. The
|
||||
// tool's project file runs within its load, and so does every sibling it
|
||||
// loads in turn, which is what LoadingHostTooling() reports to GitProject
|
||||
// and LocalProject. Per thread like LoadingFrames: a sibling loaded on a
|
||||
// thread of the tool's own goes uncounted, as it gets no ProjectArgs().
|
||||
thread_local std::int32_t HostToolingLoads = 0;
|
||||
|
||||
class HostToolingScope {
|
||||
public:
|
||||
HostToolingScope() { ++HostToolingLoads; }
|
||||
~HostToolingScope() { --HostToolingLoads; }
|
||||
HostToolingScope(const HostToolingScope&) = delete;
|
||||
HostToolingScope& operator=(const HostToolingScope&) = delete;
|
||||
};
|
||||
|
||||
// A pre-dependency is host tooling: project.so is loaded into the running
|
||||
// crafter-build, so the dependency is built for the host and against the
|
||||
// same PCM cache LoadProject populates — the hostConfig LoadProject itself
|
||||
// compiles project.so with, so the dependency's std/Crafter.Build PCMs
|
||||
// land in the cache dir project.so is built against. Recursive, because
|
||||
// the dependency's project.cpp forwards no arguments but still reads
|
||||
// CRAFTER_BUILD_MARCH from the environment for its own deps.
|
||||
// the dependency's project.cpp is loaded with no arguments and so passes
|
||||
// none on, while its own deps still read CRAFTER_BUILD_MARCH from the
|
||||
// environment.
|
||||
void ForceHostTooling(Configuration& cfg, const Configuration& host, std::unordered_set<Configuration*>& seen) {
|
||||
if (!seen.insert(&cfg).second) return;
|
||||
cfg.target = host.target;
|
||||
|
|
@ -502,24 +523,36 @@ namespace {
|
|||
std::set<std::string> libs;
|
||||
|
||||
for (const PragmaDependency& pragma : pragmas) {
|
||||
// Neither spec sets args, and the tool still gets none of the
|
||||
// consumer's: this pre-pass runs before LoadProject hands the
|
||||
// frame its args, so ProjectArgs() is empty. Given the consumer's
|
||||
// --target=, the tool's project.cpp would pick sources and flags
|
||||
// for the wrong triple, and its flags would split the tool's
|
||||
// variant from the one every other consumer shares. The scope
|
||||
// keeps the tool and everything it loads in a project cache of
|
||||
// their own: ForceHostTooling rewrites them in place, and a
|
||||
// consumer loading the same sibling for itself got that host
|
||||
// build back whenever the two cache keys matched.
|
||||
Configuration* cfg = nullptr;
|
||||
if (IsGitSource(pragma.source)) {
|
||||
GitProjectSpec spec;
|
||||
spec.source = { pragma.source, pragma.branch, pragma.commit };
|
||||
if (!pragma.project.empty()) spec.projectFile = pragma.project;
|
||||
cfg = GitProject(spec);
|
||||
} else {
|
||||
if (!pragma.branch.empty() || !pragma.commit.empty()) PragmaError(absProject, pragma.line, "'branch' and 'commit' only apply to a git source");
|
||||
// Relative to the project file, not the cwd: LoadProject runs
|
||||
// from wherever the consumer was invoked.
|
||||
fs::path local = (absProject.parent_path() / pragma.source).lexically_normal();
|
||||
if (fs::is_directory(local)) {
|
||||
local /= pragma.project.empty() ? fs::path("project.cpp") : fs::path(pragma.project);
|
||||
} else if (!pragma.project.empty()) {
|
||||
PragmaError(absProject, pragma.line, "'project' only applies to a directory source");
|
||||
{
|
||||
HostToolingScope hostTooling;
|
||||
if (IsGitSource(pragma.source)) {
|
||||
GitProjectSpec spec{ .source = { pragma.source, pragma.branch, pragma.commit } };
|
||||
if (!pragma.project.empty()) spec.projectFile = pragma.project;
|
||||
cfg = GitProject(spec);
|
||||
} else {
|
||||
if (!pragma.branch.empty() || !pragma.commit.empty()) PragmaError(absProject, pragma.line, "'branch' and 'commit' only apply to a git source");
|
||||
// Relative to the project file, not the cwd: LoadProject runs
|
||||
// from wherever the consumer was invoked.
|
||||
fs::path local = (absProject.parent_path() / pragma.source).lexically_normal();
|
||||
if (fs::is_directory(local)) {
|
||||
local /= pragma.project.empty() ? fs::path("project.cpp") : fs::path(pragma.project);
|
||||
} else if (!pragma.project.empty()) {
|
||||
PragmaError(absProject, pragma.line, "'project' only applies to a directory source");
|
||||
}
|
||||
if (!fs::exists(local)) PragmaError(absProject, pragma.line, std::format("no such project file: {}", local.string()));
|
||||
cfg = LocalProject({ .projectFile = local });
|
||||
}
|
||||
if (!fs::exists(local)) PragmaError(absProject, pragma.line, std::format("no such project file: {}", local.string()));
|
||||
cfg = LocalProject({ .projectFile = local });
|
||||
}
|
||||
if (std::ranges::contains(out.configs, cfg)) continue;
|
||||
|
||||
|
|
@ -596,6 +629,16 @@ std::vector<Configuration*> Crafter::PragmaDependencies(const fs::path& projectF
|
|||
return {};
|
||||
}
|
||||
|
||||
std::vector<std::string> Crafter::ProjectArgs() {
|
||||
if (LoadingFrames.empty()) return {};
|
||||
const std::span<const std::string_view> args = LoadingFrames.back().args;
|
||||
return std::vector<std::string>(args.begin(), args.end());
|
||||
}
|
||||
|
||||
bool Crafter::LoadingHostTooling() {
|
||||
return HostToolingLoads > 0;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// The one reading of a wait status (pclose, system) for every helper
|
||||
// that runs a command: a signal death is reported the way a shell
|
||||
|
|
@ -971,6 +1014,13 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const
|
|||
WriteStamp(StampPath(soPath), projectStamp);
|
||||
}
|
||||
|
||||
// Before dlopen, so the file's static initialisers can already call
|
||||
// ProjectDependency(): its dependencies are the same on every load.
|
||||
{
|
||||
std::lock_guard lock(PragmaDependenciesMutex);
|
||||
PragmaDependenciesByProject[absProject] = preDeps.configs;
|
||||
}
|
||||
guard.Frame().dependencies = std::move(preDeps.configs);
|
||||
void* handle = dlopen(soPath.c_str(), RTLD_NOW | RTLD_GLOBAL);
|
||||
if (!handle) {
|
||||
throw std::runtime_error(std::format("Failed to load project {}: {}", soPath.string(), dlerror()));
|
||||
|
|
@ -983,10 +1033,12 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const
|
|||
throw std::runtime_error(std::format("CrafterBuildProject not found in {}: {}", soPath.string(), err));
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard lock(PragmaDependenciesMutex);
|
||||
PragmaDependenciesByProject[absProject] = preDeps.configs;
|
||||
}
|
||||
guard.Frame().dependencies = std::move(preDeps.configs);
|
||||
// The args only for the call itself, unlike the dependencies. A static
|
||||
// initialiser runs once per process, on the first load of the file: a
|
||||
// later load gets the same mapping back from dlopen, so a spec built at
|
||||
// namespace scope kept the first load's args for every load after it.
|
||||
// Now it gets none, whichever load ran it. The pragma pre-pass above
|
||||
// also ran on this frame and relies on the args not being there yet.
|
||||
guard.Frame().args = args;
|
||||
return fn(args);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue