Skip to content

Commit 3471ff1

Browse files
typelessclaude
andcommitted
Replace BuildMode enum with composable filter logic
Eliminates the BuildMode enum (Full, Incremental, Targets, Subset, ScopeWithUpstream) and determine_build_mode() function. Each build constraint (incremental, targets, scope) computes a NodeIdMap32 filter independently, then filters are intersected when multiple apply. This fixes the bug where `putup -B build build/putup` built all 98 commands instead of just the 57 needed for the putup binary. The incremental and target constraints now compose naturally. Move collect_affected_commands and collect_required_commands from scheduler.cpp anonymous namespace to pup::graph (dag.hpp/dag.cpp) where they belong as graph algorithms. Simplify scheduler public API from 4 build methods to 1: build(state, filter=nullptr) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5195f43 commit 3471ff1

6 files changed

Lines changed: 154 additions & 239 deletions

File tree

include/pup/exec/scheduler.hpp

Lines changed: 2 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -89,31 +89,9 @@ class Scheduler final {
8989
Scheduler(Scheduler&&) noexcept;
9090
auto operator=(Scheduler&&) noexcept -> Scheduler&;
9191

92-
/// Build from a dependency graph
92+
/// Build commands matching the filter. nullptr = build all.
9393
[[nodiscard]]
94-
auto build(graph::BuildState const& graph) -> Result<BuildStats>;
95-
96-
/// Build only the commands that depend on changed files
97-
[[nodiscard]]
98-
auto build_incremental(
99-
graph::BuildState const& graph,
100-
Vec<StringId> const& changed_files
101-
) -> Result<BuildStats>;
102-
103-
/// Build only a specific subset of commands
104-
[[nodiscard]]
105-
auto build_subset(
106-
graph::BuildState const& graph,
107-
NodeIdMap32 const& command_ids
108-
) -> Result<BuildStats>;
109-
110-
/// Build specific targets and all required dependencies.
111-
/// Uses reverse traversal to find commands needed.
112-
[[nodiscard]]
113-
auto build_targets(
114-
graph::BuildState const& graph,
115-
Vec<NodeId> const& target_ids
116-
) -> Result<BuildStats>;
94+
auto build(graph::BuildState const& state, NodeIdMap32 const* filter = nullptr) -> Result<BuildStats>;
11795

11896
/// Set callback for job start
11997
auto on_job_start(JobStartCallback callback) -> void;
@@ -140,14 +118,6 @@ class Scheduler final {
140118
private:
141119
std::unique_ptr<Impl> impl_;
142120

143-
/// Core build loop shared by all public build methods.
144-
/// When filter is non-null, only matching commands run.
145-
[[nodiscard]]
146-
auto run(
147-
graph::BuildState const& graph,
148-
NodeIdMap32 const* filter
149-
) -> Result<BuildStats>;
150-
151121
/// Execute jobs in parallel using the async event loop
152122
auto execute_parallel(
153123
Vec<BuildJob> const& jobs,

include/pup/graph/dag.hpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,16 @@ struct BuildState {
357357
[[nodiscard]]
358358
auto make_build_state() -> BuildState;
359359

360+
/// Collect all commands affected by the given changed files.
361+
/// Uses forward traversal: starts at changed inputs, walks forward through outputs.
362+
[[nodiscard]]
363+
auto collect_affected_commands(Graph const& graph, Vec<StringId> const& changed_files) -> NodeIdMap32;
364+
365+
/// Collect all commands required to build the given target nodes.
366+
/// Uses reverse traversal: starts at targets, walks backward through inputs.
367+
[[nodiscard]]
368+
auto collect_required_commands(Graph const& graph, Vec<NodeId> const& target_ids) -> NodeIdMap32;
369+
360370
/// Set build root name and clear path cache
361371
auto set_build_root_name(BuildState& state, std::string_view name) -> void;
362372

src/cli/cmd_build.cpp

Lines changed: 42 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,40 +1037,19 @@ auto remove_stale_outputs(
10371037
}
10381038
}
10391039

1040-
/// Build mode precedence (highest to lowest):
1041-
/// 1. Incremental - if old index exists and files changed
1042-
/// 2. ScopeWithUpstream - fresh build with -a and explicit targets
1043-
/// 3. Targets - if specific output targets requested (and not incremental)
1044-
/// 4. Subset - exclude config commands from full build
1045-
/// 5. Full - build everything
1046-
enum class BuildMode {
1047-
Incremental,
1048-
ScopeWithUpstream,
1049-
Targets,
1050-
Subset,
1051-
Full,
1052-
};
1053-
1054-
auto determine_build_mode(
1055-
bool has_targets,
1056-
bool use_incremental,
1057-
bool has_config_cmds,
1058-
bool scope_with_upstream
1059-
) -> BuildMode
1040+
auto intersect_filters(
1041+
pup::NodeIdMap32 const& a,
1042+
pup::NodeIdMap32 const& b,
1043+
pup::graph::Graph const& graph
1044+
) -> pup::NodeIdMap32
10601045
{
1061-
if (use_incremental) {
1062-
return BuildMode::Incremental;
1063-
}
1064-
if (scope_with_upstream) {
1065-
return BuildMode::ScopeWithUpstream;
1066-
}
1067-
if (has_targets) {
1068-
return BuildMode::Targets;
1069-
}
1070-
if (has_config_cmds) {
1071-
return BuildMode::Subset;
1046+
auto result = pup::NodeIdMap32 {};
1047+
for (auto id : pup::graph::all_nodes(graph)) {
1048+
if (a.contains(id) && b.contains(id)) {
1049+
result.set(id, 1);
1050+
}
10721051
}
1073-
return BuildMode::Full;
1052+
return result;
10741053
}
10751054

10761055
/// Build a single variant with the given options.
@@ -1359,46 +1338,49 @@ auto build_single_variant(
13591338
}
13601339

13611340
auto start = pup::SteadyClock::time_point { pup::SteadyClock::now() };
1362-
auto build_result = pup::Result<pup::exec::BuildStats> {};
1363-
1364-
auto scope_with_upstream = opts.include_all_deps && !scopes.empty() && !use_incremental;
1365-
auto mode = determine_build_mode(
1366-
!target_node_ids.empty(),
1367-
use_incremental,
1368-
!config_cmds.empty(),
1369-
scope_with_upstream
1370-
);
13711341

1372-
switch (mode) {
1373-
case BuildMode::Incremental: {
1374-
build_result = scheduler.build_incremental(bs, changed_files);
1375-
break;
1342+
// Composable filter: layer independent concerns, intersect when combined
1343+
auto filter = pup::NodeIdMap32 {};
1344+
auto has_filter = false;
1345+
1346+
if (use_incremental && !changed_files.empty()) {
1347+
filter = pup::graph::collect_affected_commands(bs.graph, changed_files);
1348+
has_filter = true;
13761349
}
1377-
case BuildMode::ScopeWithUpstream: {
1350+
1351+
if (!target_node_ids.empty()) {
1352+
auto required = pup::graph::collect_required_commands(bs.graph, target_node_ids);
1353+
if (has_filter) {
1354+
filter = intersect_filters(filter, required, bs.graph);
1355+
} else {
1356+
filter = std::move(required);
1357+
has_filter = true;
1358+
}
1359+
}
1360+
1361+
if (opts.include_all_deps && !scopes.empty() && !use_incremental) {
13781362
auto scope_cmds = collect_scope_with_upstream_commands(bs, scopes);
13791363
for (auto const& cfg : config_cmds) {
13801364
scope_cmds.remove(cfg.cmd_id);
13811365
}
1382-
build_result = scheduler.build_subset(bs, scope_cmds);
1383-
break;
1366+
if (has_filter) {
1367+
filter = intersect_filters(filter, scope_cmds, bs.graph);
1368+
} else {
1369+
filter = std::move(scope_cmds);
1370+
has_filter = true;
1371+
}
13841372
}
1385-
case BuildMode::Targets:
1386-
build_result = scheduler.build_targets(bs, target_node_ids);
1387-
break;
1388-
case BuildMode::Subset: {
1389-
auto non_config_cmds = pup::NodeIdMap32 {};
1373+
1374+
if (!config_cmds.empty() && !has_filter) {
13901375
for (auto id : pup::graph::all_nodes(bs.graph)) {
13911376
if (node_id::is_command(id) && !config_cmd_ids.contains(id)) {
1392-
non_config_cmds.set(id, 1);
1377+
filter.set(id, 1);
13931378
}
13941379
}
1395-
build_result = scheduler.build_subset(bs, non_config_cmds);
1396-
break;
1397-
}
1398-
case BuildMode::Full:
1399-
build_result = scheduler.build(bs);
1400-
break;
1380+
has_filter = true;
14011381
}
1382+
1383+
auto build_result = scheduler.build(bs, has_filter ? &filter : nullptr);
14021384
auto end = pup::SteadyClock::time_point { pup::SteadyClock::now() };
14031385
auto duration = std::chrono::milliseconds { std::chrono::duration_cast<std::chrono::milliseconds>(end - start) };
14041386

src/cli/cmd_configure.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ auto configure_single_variant(
182182
}
183183
});
184184

185-
auto build_result = scheduler.build_subset(ctx.graph(), all_commands);
185+
auto build_result = scheduler.build(ctx.graph(), &all_commands);
186186

187187
if (!opts.verbose) {
188188
printf("\n");

src/exec/scheduler.cpp

Lines changed: 1 addition & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -329,113 +329,6 @@ auto validate_guard_dependencies(
329329
return {};
330330
}
331331

332-
/// Collect all commands required to build the given target nodes.
333-
/// Uses reverse traversal: starts at targets, walks backward through inputs.
334-
auto collect_required_commands(
335-
graph::Graph const& graph,
336-
Vec<NodeId> const& target_ids
337-
) -> NodeIdMap32
338-
{
339-
auto visited = NodeIdMap32 {};
340-
auto commands = NodeIdMap32 {};
341-
auto stack = Vec<NodeId> {};
342-
for (auto id : target_ids) {
343-
stack.push_back(id);
344-
}
345-
346-
while (!stack.empty()) {
347-
auto id = stack.back();
348-
stack.pop_back();
349-
350-
if (visited.contains(id)) {
351-
continue;
352-
}
353-
visited.set(id, 1);
354-
355-
if (node_id::is_command(id) && graph::get_command_node(graph, id)) {
356-
commands.set(id, 1);
357-
}
358-
359-
for (auto input_id : graph::get_inputs(graph, id)) {
360-
stack.push_back(input_id);
361-
}
362-
363-
for (auto dep_id : graph::get_order_only(graph, id)) {
364-
stack.push_back(dep_id);
365-
}
366-
}
367-
368-
return commands;
369-
}
370-
371-
/// Compute all commands affected by the given changed files.
372-
/// Uses forward traversal: starts at changed inputs, walks forward through outputs.
373-
auto collect_affected_commands(
374-
graph::Graph const& graph,
375-
Vec<StringId> const& changed_files
376-
) -> NodeIdMap32
377-
{
378-
auto& pool = global_pool();
379-
380-
auto path_to_id = Vec<std::pair<std::string_view, NodeId>> {};
381-
for (auto id : graph::all_nodes(graph)) {
382-
auto path_id = graph::get_full_path(graph, id);
383-
if (!is_empty(path_id)) {
384-
path_to_id.emplace_back(pool.get(path_id), id);
385-
}
386-
}
387-
std::sort(path_to_id.begin(), path_to_id.end());
388-
389-
auto affected = NodeIdMap32 {};
390-
auto to_process = Vec<NodeId> {};
391-
392-
for (auto file_id : changed_files) {
393-
auto file_path = pool.get(file_id);
394-
auto it = std::lower_bound(path_to_id.begin(), path_to_id.end(), file_path, [](auto const& p, auto const& k) { return p.first < k; });
395-
if (it != path_to_id.end() && it->first == file_path) {
396-
auto id = it->second;
397-
if (!affected.contains(id)) {
398-
affected.set(id, 1);
399-
to_process.push_back(id);
400-
}
401-
402-
auto const* node = graph::get_file_node(graph, id);
403-
if (node && node->type == NodeType::Generated) {
404-
for (auto input_id : graph::get_inputs(graph, id)) {
405-
if (!affected.contains(input_id)) {
406-
affected.set(input_id, 1);
407-
to_process.push_back(input_id);
408-
}
409-
}
410-
}
411-
}
412-
}
413-
414-
// Expand to include all dependent commands (including order-only).
415-
// get_outputs() excludes sticky edges by design (Tupfile/config dependencies
416-
// are parse-time deps, not build-time deps).
417-
while (!to_process.empty()) {
418-
auto id = NodeId { to_process.back() };
419-
to_process.pop_back();
420-
421-
for (auto dep_id : graph::get_outputs(graph, id)) {
422-
if (!affected.contains(dep_id)) {
423-
affected.set(dep_id, 1);
424-
to_process.push_back(dep_id);
425-
}
426-
}
427-
428-
for (auto dep_id : graph::get_order_only_dependents(graph, id)) {
429-
if (!affected.contains(dep_id)) {
430-
affected.set(dep_id, 1);
431-
to_process.push_back(dep_id);
432-
}
433-
}
434-
}
435-
436-
return affected;
437-
}
438-
439332
struct JobSlot {
440333
pup::platform::AsyncProcess process = {};
441334
HeapBuf stdout_buf = {};
@@ -609,45 +502,7 @@ auto Scheduler::stats() const -> BuildStats
609502
// Public build API
610503
// ---------------------------------------------------------------------------
611504

612-
auto Scheduler::build(graph::BuildState const& state) -> Result<BuildStats>
613-
{
614-
return run(state, nullptr);
615-
}
616-
617-
auto Scheduler::build_incremental(
618-
graph::BuildState const& state,
619-
Vec<StringId> const& changed_files
620-
) -> Result<BuildStats>
621-
{
622-
auto affected = collect_affected_commands(state.graph, changed_files);
623-
return run(state, &affected);
624-
}
625-
626-
auto Scheduler::build_subset(
627-
graph::BuildState const& state,
628-
NodeIdMap32 const& command_ids
629-
) -> Result<BuildStats>
630-
{
631-
return run(state, &command_ids);
632-
}
633-
634-
auto Scheduler::build_targets(
635-
graph::BuildState const& state,
636-
Vec<NodeId> const& target_ids
637-
) -> Result<BuildStats>
638-
{
639-
auto cmds = collect_required_commands(state.graph, target_ids);
640-
return run(state, &cmds);
641-
}
642-
643-
// ---------------------------------------------------------------------------
644-
// Private implementation
645-
// ---------------------------------------------------------------------------
646-
647-
auto Scheduler::run(
648-
graph::BuildState const& state,
649-
NodeIdMap32 const* filter
650-
) -> Result<BuildStats>
505+
auto Scheduler::build(graph::BuildState const& state, NodeIdMap32 const* filter) -> Result<BuildStats>
651506
{
652507
impl_->cancelled = false;
653508
impl_->stats = BuildStats {};

0 commit comments

Comments
 (0)