From ead76b4e38424bc370e05578e0cb3616115664ef Mon Sep 17 00:00:00 2001 From: John Ericson Date: Sat, 25 Apr 2026 16:22:35 -0400 Subject: [PATCH 1/6] refactor: Clean up `log.tt` template Compute URLs and title strings upfront instead of repeating inline ternaries throughout the template. Use consistent `${...}` variable interpolation style. No behavior changes. --- subprojects/hydra/root/log.tt | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/subprojects/hydra/root/log.tt b/subprojects/hydra/root/log.tt index 783927fe8..3c651269a 100644 --- a/subprojects/hydra/root/log.tt +++ b/subprojects/hydra/root/log.tt @@ -1,7 +1,20 @@ -[% WRAPPER layout.tt - titleHTML="Log of " _ (step ? " step $step.stepnr of " : "") _ "build ${build.id} of job " _ linkToJob(build.jobset, job) - title="Log of " _ (step ? " step $step.stepnr of " : "") _ "build ${build.id} of job " _ makeNameTextForJob(build.jobset, job) +[% + buildUrl = c.uri_for('/build', build.id); + IF step; + logUrl = c.uri_for('/build', build.id, 'nixlog', step.stepnr); + drvpath = step.drvpath; + titleText = "Log of step ${step.stepnr} of build ${build.id} of job " _ makeNameTextForJob(build.jobset, job); + titleLink = "Log of step ${step.stepnr} of build ${build.id} of job " _ linkToJob(build.jobset, job); + ELSE; + logUrl = c.uri_for('/build', build.id, 'log'); + drvpath = build.drvpath; + titleText = "Log of build ${build.id} of job " _ makeNameTextForJob(build.jobset, job); + titleLink = "Log of build ${build.id} of job " _ linkToJob(build.jobset, job); + END; + rawUrl = logUrl _ '/raw'; %] + +[% WRAPPER layout.tt titleHTML=titleLink title=titleText %] [% PROCESS common.tt %]

@@ -11,14 +24,13 @@ [% ELSE %] is [% END %] - the build log ( step ? c.uri_for('/build' build.id 'nixlog' step.stepnr, 'raw') - : c.uri_for('/build' build.id 'log', 'raw')) %]>raw) of derivation [% IF step; step.drvpath; ELSE; build.drvpath; END %]. + the build log ( rawUrl) %]>raw) + of derivation [% drvpath | html %]. [% IF step && step.machine %] It was built on [% step.machine | html %]. [% END %] [% IF tail %] - The step ? c.uri_for('/build' build.id 'nixlog' step.stepnr) - : c.uri_for('/build' build.id 'log')) %]>full log is also available. + The logUrl) %]>full log is also available. [% END %]

From 779375dc7b40c3df3cd3235f94bef03d87c496aa Mon Sep 17 00:00:00 2001 From: John Ericson Date: Sat, 25 Apr 2026 16:33:12 -0400 Subject: [PATCH 2/6] feat: Make build link clickable in log page title --- subprojects/hydra/root/log.tt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/subprojects/hydra/root/log.tt b/subprojects/hydra/root/log.tt index 3c651269a..999c849e0 100644 --- a/subprojects/hydra/root/log.tt +++ b/subprojects/hydra/root/log.tt @@ -4,12 +4,15 @@ logUrl = c.uri_for('/build', build.id, 'nixlog', step.stepnr); drvpath = step.drvpath; titleText = "Log of step ${step.stepnr} of build ${build.id} of job " _ makeNameTextForJob(build.jobset, job); - titleLink = "Log of step ${step.stepnr} of build ${build.id} of job " _ linkToJob(build.jobset, job); + titleLink = "Log of step ${step.stepnr}" + _ " of build ${build.id}" + _ " of job " _ linkToJob(build.jobset, job); ELSE; logUrl = c.uri_for('/build', build.id, 'log'); drvpath = build.drvpath; titleText = "Log of build ${build.id} of job " _ makeNameTextForJob(build.jobset, job); - titleLink = "Log of build ${build.id} of job " _ linkToJob(build.jobset, job); + titleLink = "Log of build ${build.id}" + _ " of job " _ linkToJob(build.jobset, job); END; rawUrl = logUrl _ '/raw'; %] From 219a8cafdd018b71cef986cef59b9492c437770c Mon Sep 17 00:00:00 2001 From: John Ericson Date: Sat, 25 Apr 2026 15:10:48 -0400 Subject: [PATCH 3/6] refactor: Extract shared blocks for reuse by build step page Soon, we're going to make a new build step page which will share a number of things with the build page. In order to get ready for that, extract the following into a new `build-common.tt` for reuse: - `renderOutputs` - `renderStepStatus` - `renderStepDuration` - `renderStepMachine` - `renderLogButtons` Likewise, move the `showLog` helper out of `Build.pm` into `Hydra::Helper::LogEndpoints`. Both are needed by the BuildStep controller introduced in a later commit. As a bonus, the new `renderLogButtons` is already used twice within `build.tt` itself (for the build log and the runcommand log), deduplicating previously identical button markup. No behavior changes. --- .../hydra/lib/Hydra/Controller/Build.pm | 26 +------ .../hydra/lib/Hydra/Helper/LogEndpoints.pm | 36 ++++++++++ subprojects/hydra/root/build-common.tt | 68 +++++++++++++++++++ subprojects/hydra/root/build.tt | 66 ++---------------- 4 files changed, 111 insertions(+), 85 deletions(-) create mode 100644 subprojects/hydra/lib/Hydra/Helper/LogEndpoints.pm create mode 100644 subprojects/hydra/root/build-common.tt diff --git a/subprojects/hydra/lib/Hydra/Controller/Build.pm b/subprojects/hydra/lib/Hydra/Controller/Build.pm index ca9a22cff..d46f36e49 100644 --- a/subprojects/hydra/lib/Hydra/Controller/Build.pm +++ b/subprojects/hydra/lib/Hydra/Controller/Build.pm @@ -6,6 +6,7 @@ use warnings; use base 'Hydra::Base::Controller::NixChannel'; use Hydra::Helper::Nix; use Hydra::Helper::CatalystUtils; +use Hydra::Helper::LogEndpoints; use File::Basename; use File::LibMagic; use File::stat; @@ -166,31 +167,6 @@ sub view_runcommandlog : Chained('buildChain') PathPart('runcommandlog') { } -sub showLog { - my ($c, $mode, $log_uri) = @_; - $mode //= "pretty"; - - if ($mode eq "pretty") { - $c->stash->{log_uri} = $log_uri; - $c->stash->{template} = 'log.tt'; - } - - elsif ($mode eq "raw") { - $c->res->redirect($log_uri); - } - - elsif ($mode eq "tail") { - my $lines = 50; - $c->stash->{log_uri} = $log_uri . "?tail=$lines"; - $c->stash->{tail} = $lines; - $c->stash->{template} = 'log.tt'; - } - - else { - error($c, "Unknown log display mode '$mode'."); - } -} - sub defaultUriForProduct { my ($self, $c, $product, @path) = @_; diff --git a/subprojects/hydra/lib/Hydra/Helper/LogEndpoints.pm b/subprojects/hydra/lib/Hydra/Helper/LogEndpoints.pm new file mode 100644 index 000000000..685737498 --- /dev/null +++ b/subprojects/hydra/lib/Hydra/Helper/LogEndpoints.pm @@ -0,0 +1,36 @@ +package Hydra::Helper::LogEndpoints; + +use strict; +use warnings; +use Exporter; +use Hydra::Helper::CatalystUtils; + +our @ISA = qw(Exporter); +our @EXPORT = qw(showLog); + +sub showLog { + my ($c, $mode, $log_uri) = @_; + $mode //= "pretty"; + + if ($mode eq "pretty") { + $c->stash->{log_uri} = $log_uri; + $c->stash->{template} = 'log.tt'; + } + + elsif ($mode eq "raw") { + $c->res->redirect($log_uri); + } + + elsif ($mode eq "tail") { + my $lines = 50; + $c->stash->{log_uri} = $log_uri . "?tail=$lines"; + $c->stash->{tail} = $lines; + $c->stash->{template} = 'log.tt'; + } + + else { + error($c, "Unknown log display mode '$mode'."); + } +} + +1; diff --git a/subprojects/hydra/root/build-common.tt b/subprojects/hydra/root/build-common.tt new file mode 100644 index 000000000..66a5a342e --- /dev/null +++ b/subprojects/hydra/root/build-common.tt @@ -0,0 +1,68 @@ +[% BLOCK renderLogButtons %] + url) %]>pretty + url _ "/raw") %]>raw + url _ "/tail") %]>tail +[% END %] + +[% BLOCK renderOutputs %] + [% start=1; FOREACH output IN outputs %] + [% IF !start %],
[% END; start=0; output.path %] + [% END %] +[% END %] + +[% BLOCK renderStepDuration %] + [% IF step.busy == 0 %] + [% IF step.stoptime %] + [% INCLUDE renderDuration duration = step.stoptime - step.starttime %] + [% ELSE %] + n/a + [% END %] + [% ELSIF build.finished %] + [% INCLUDE renderDuration duration = build.stoptime - step.starttime %] + [% ELSE %] + [% INCLUDE renderDuration duration = curTime - step.starttime %] + [% END %] +[% END %] + +[% BLOCK renderStepMachine %] + [% IF step.busy != 0 || ((step.machine || step.starttime) && (step.status == 0 || step.status == 1 || step.status == 3 || step.status == 4 || step.status == 7)) %] + [% INCLUDE renderMachineName machine=step.machine %] + [% ELSE %] + n/a + [% END %] +[% END %] + +[% BLOCK renderStepStatus %] + [% IF step.busy != 0 %] + [% INCLUDE renderBusyStatus %] + [% ELSIF step.status == 0 %] + [% IF step.isnondeterministic %] + Succeeded with non-determistic result + [% ELSE %] + Succeeded + [% END %] + [% IF step.timesbuilt && step.timesbuilt > 1 %] + ([% step.timesbuilt %] times) + [% END %] + [% ELSIF step.status == 3 %] + Aborted[% IF step.errormsg %]: [% HTML.escape(step.errormsg) %][% END %] + [% ELSIF step.status == 4 %] + Cancelled + [% ELSIF step.status == 7 %] + Timed out + [% ELSIF step.status == 8 %] + Cached failure + [% ELSIF step.status == 9 %] + Unsupported system type + [% ELSIF step.status == 10 %] + Log limit exceeded + [% ELSIF step.status == 11 %] + Output limit exceeded + [% ELSIF step.status == 12 %] + Non-determinism detected[% IF step.timesbuilt %] after [% HTML.escape(step.timesbuilt) %] times[% END %] + [% ELSIF step.errormsg %] + Failed: [% HTML.escape(step.errormsg) %] + [% ELSE %] + Failed + [% END %] +[% END %] diff --git a/subprojects/hydra/root/build.tt b/subprojects/hydra/root/build.tt index a5a817d1b..b73f6312c 100644 --- a/subprojects/hydra/root/build.tt +++ b/subprojects/hydra/root/build.tt @@ -3,6 +3,7 @@ titleHTML="Build $id of job " _ linkToJob(jobset, job) %] [% PROCESS common.tt %] +[% PROCESS "build-common.tt" %] [% PROCESS "product-list.tt" %] [% USE HTML %] [% USE Date %] @@ -19,12 +20,6 @@ FOR step IN steps; END; %] -[% BLOCK renderOutputs %] - [% start=1; FOREACH output IN outputs %] - [% IF !start %],
[% END; start=0; output.path %] - [% END %] -[% END %] - [% BLOCK renderBuildSteps %] @@ -45,53 +40,10 @@ END; Substitution of [% INCLUDE renderOutputs outputs=step.buildstepoutputs %] [% END %] - - + + @@ -226,9 +178,7 @@ END; [% END %] @@ -531,11 +481,7 @@ END; [% END %] [% IF runcommandlog.uuid != undef %] [% runLog = c.uri_for('/build', build.id, 'runcommandlog', runcommandlog.uuid) %] -
- runLog) %]>pretty - runLog) %]/raw">raw - runLog) %]/tail">tail -
+
[% INCLUDE renderLogButtons url=runLog %]
[% END %] [% ELSE %] From 93432dc0265d7c0fea5d2846b67c3a2dd5b692b9 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Sat, 25 Apr 2026 15:55:56 -0400 Subject: [PATCH 4/6] =?UTF-8?q?feat:=20Show=20output=20name=20=E2=86=92=20?= =?UTF-8?q?path=20in=20build=20detail=20via=20`renderOutputsTable`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a `renderOutputsTable` block in `build-common.tt` that renders outputs as a nested info-table with name and path columns. Use it on the build detail page, replacing the old inline `renderOutputs` which only showed paths. I like this much better because I want to see the output names at a glance, not just the store paths. This layout is especially good for CA derivations where we don't know the output paths in advanced of building them. --- subprojects/hydra/root/build-common.tt | 11 +++++++++++ subprojects/hydra/root/build.tt | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/subprojects/hydra/root/build-common.tt b/subprojects/hydra/root/build-common.tt index 66a5a342e..a26e88937 100644 --- a/subprojects/hydra/root/build-common.tt +++ b/subprojects/hydra/root/build-common.tt @@ -10,6 +10,17 @@ [% END %] [% END %] +[% BLOCK renderOutputsTable %] +
- [% IF step.busy == 0; - IF step.stoptime; - INCLUDE renderDuration duration = step.stoptime - step.starttime; - ELSE; - %]n/a[% - END; - ELSIF build.finished; - INCLUDE renderDuration duration = build.stoptime - step.starttime; - ELSE; - INCLUDE renderDuration duration = curTime - step.starttime; - END %] - [% IF step.busy != 0 || ((step.machine || step.starttime) && (step.status == 0 || step.status == 1 || step.status == 3 || step.status == 4 || step.status == 7)); INCLUDE renderMachineName machine=step.machine; ELSE; "n/a"; END %][% INCLUDE renderStepDuration %][% INCLUDE renderStepMachine %] - [% IF step.busy != 0 %] - [% INCLUDE renderBusyStatus %] - [% ELSIF step.status == 0 %] - [% IF step.isnondeterministic %] - Succeeded with non-determistic result - [% ELSE %] - Succeeded - [% END %] - [% IF step.timesbuilt && step.timesbuilt > 1 %] - ([% step.timesbuilt %] times) - [% END %] - [% ELSIF step.status == 3 %] - Aborted[% IF step.errormsg %]: [% HTML.escape(step.errormsg) %][% END %] - [% ELSIF step.status == 4 %] - Cancelled - [% ELSIF step.status == 7 %] - Timed out - [% ELSIF step.status == 8 %] - Cached failure - [% ELSIF step.status == 9 %] - Unsupported system type - [% ELSIF step.status == 10 %] - Log limit exceeded - [% ELSIF step.status == 11 %] - Output limit exceeded - [% ELSIF step.status == 12 %] - Non-determinism detected [% IF step.timesbuilt %] after [% HTML.escape(step.timesbuilt) %] times[% END %] - [% ELSIF step.errormsg %] - Failed: [% HTML.escape(step.errormsg) %] - [% ELSE %] - Failed - [% END %] + [% INCLUDE renderStepStatus %] [%%] [%+ IF has_log; INCLUDE renderLogLinks url=log inRow=1; END %] [%+ IF step.propagatedfrom; %](propagated from [% INCLUDE renderBuildIdLink id=step.propagatedfrom.get_column('id') %])[% END %] Logfile: [% actualLog = cachedBuildStep ? c.uri_for('/build' cachedBuild.id 'nixlog' cachedBuildStep.stepnr) : c.uri_for('/build' build.id 'log') %] - actualLog) %]>pretty - actualLog _ "/raw") %]>raw - actualLog _ "/tail") %]>tail + [% INCLUDE renderLogButtons url=actualLog %]
+ [% FOREACH output IN outputs %] + + + + + [% END %] +
[% output.name | html %][% IF output.path; output.path | html; ELSE %]n/a[% END %]
+[% END; %] + [% BLOCK renderStepDuration %] [% IF step.busy == 0 %] [% IF step.stoptime %] diff --git a/subprojects/hydra/root/build.tt b/subprojects/hydra/root/build.tt index b73f6312c..280a3a705 100644 --- a/subprojects/hydra/root/build.tt +++ b/subprojects/hydra/root/build.tt @@ -319,7 +319,7 @@ END; Output store paths: - [% INCLUDE renderOutputs outputs=build.buildoutputs %] + [% INCLUDE renderOutputsTable outputs=build.buildoutputs %] [% chartsURL = c.uri_for('/job' build.project.name build.jobset.name build.job) _ "#tabs-charts" %] [% IF build.finished && build.closuresize %] From 17aaa8d2889ad59e57f4e9ab873826344b6f635d Mon Sep 17 00:00:00 2001 From: John Ericson Date: Sat, 25 Apr 2026 14:42:14 -0400 Subject: [PATCH 5/6] feat: Create a proper build step details page Build steps are, in my view, an important concept that Hydra doesn't yet give enough attention. This is my attempt to rectify that. A new detail page at `/build/:id/step/:stepnr` shows output paths, derivation, system, machine, duration, status, and log links, reusing the shared blocks extracted in the previous commit. Clicking anywhere in the build steps table row now navigates to this page, instead of the step log page. The step log pages also have a link back to this page. Also reflecting giving build steps more status, introduce `Hydra::Controller::BuildStep`, chained off `/build/buildChain`, giving them a first-class controller. For a bit of back story, note that in the future, we might switch associating build steps more with derivations than builds. This reflects that we don't really care *why* something was scheduled (the build/root derivation) as much as *what* was scheduled, especially when multiple new builds would "race" to schedule the same derivation. It also bodes well for a future where Hydra can act as a Nix derivation that receives ad-hoc build requests, so the "build" in this case would be rather lacking in metadata. Both these scenarios point to a world where `BuildStep`s become more important than `Build`s, building (ahem) atop this refactor. The step log handling is now better suited to live as part of this controller. Accordingly, it is moved from `/build/:id/nixlog/:stepnr[/raw|/tail]` to `/build/:id/step/:stepnr/log[/raw|/tail]`. The old `nixlog` URLs are preserved as 301 redirects in `Build.pm`. All templates updated to generate the new canonical URLs. --- .../hydra/lib/Hydra/Controller/Build.pm | 15 ++--- .../hydra/lib/Hydra/Controller/BuildStep.pm | 38 ++++++++++++ subprojects/hydra/root/build-step.tt | 61 +++++++++++++++++++ subprojects/hydra/root/build.tt | 9 +-- subprojects/hydra/root/deps.tt | 2 +- subprojects/hydra/root/log.tt | 5 +- subprojects/hydra/root/machine-status.tt | 2 +- subprojects/hydra/root/steps.tt | 2 +- 8 files changed, 115 insertions(+), 19 deletions(-) create mode 100644 subprojects/hydra/lib/Hydra/Controller/BuildStep.pm create mode 100644 subprojects/hydra/root/build-step.tt diff --git a/subprojects/hydra/lib/Hydra/Controller/Build.pm b/subprojects/hydra/lib/Hydra/Controller/Build.pm index d46f36e49..43cd0a8f6 100644 --- a/subprojects/hydra/lib/Hydra/Controller/Build.pm +++ b/subprojects/hydra/lib/Hydra/Controller/Build.pm @@ -134,17 +134,13 @@ sub constituents_GET { } -sub view_nixlog : Chained('buildChain') PathPart('nixlog') { +# Redirect old /build/:id/nixlog/:stepnr[/:mode] URLs to new canonical paths +sub nixlog_redirect : Chained('buildChain') PathPart('nixlog') { my ($self, $c, $stepnr, $mode) = @_; - my $step = $c->stash->{build}->buildsteps->find({stepnr => $stepnr}); - notFound($c, "Build doesn't have a build step $stepnr.") if !defined $step; - - $c->stash->{step} = $step; - - my $drvPath = $step->drvpath; - my $log_uri = $c->uri_for($c->controller('Root')->action_for("log"), [WWW::Form::UrlEncoded::PP::url_encode(basename($drvPath))]); - showLog($c, $mode, $log_uri); + my @path = ('/build', $c->stash->{id}, 'step', $stepnr, 'log'); + push @path, $mode if defined $mode; + $c->res->redirect($c->uri_for(@path), 301); } @@ -167,7 +163,6 @@ sub view_runcommandlog : Chained('buildChain') PathPart('runcommandlog') { } - sub defaultUriForProduct { my ($self, $c, $product, @path) = @_; my $x = $product->productnr diff --git a/subprojects/hydra/lib/Hydra/Controller/BuildStep.pm b/subprojects/hydra/lib/Hydra/Controller/BuildStep.pm new file mode 100644 index 000000000..6ba585601 --- /dev/null +++ b/subprojects/hydra/lib/Hydra/Controller/BuildStep.pm @@ -0,0 +1,38 @@ +package Hydra::Controller::BuildStep; + +use utf8; +use strict; +use warnings; +use base 'Hydra::Base::Controller::REST'; +use Hydra::Helper::CatalystUtils; +use Hydra::Helper::LogEndpoints; +use File::Basename; +use WWW::Form::UrlEncoded::PP qw(); + + +sub buildStepChain :Chained('/build/buildChain') :PathPart('step') :CaptureArgs(1) { + my ($self, $c, $stepnr) = @_; + + my $step = $c->stash->{build}->buildsteps->find({stepnr => $stepnr}); + notFound($c, "Build doesn't have a build step $stepnr.") if !defined $step; + + $c->stash->{step} = $step; +} + + +sub buildStep : Chained('buildStepChain') PathPart('') Args(0) { + my ($self, $c) = @_; + $c->stash->{template} = 'build-step.tt'; +} + + +sub view_nixlog : Chained('buildStepChain') PathPart('log') { + my ($self, $c, $mode) = @_; + + my $drvPath = $c->stash->{step}->drvpath; + my $log_uri = $c->uri_for($c->controller('Root')->action_for("log"), [WWW::Form::UrlEncoded::PP::url_encode(basename($drvPath))]); + showLog($c, $mode, $log_uri); +} + + +1; diff --git a/subprojects/hydra/root/build-step.tt b/subprojects/hydra/root/build-step.tt new file mode 100644 index 000000000..38ce9a906 --- /dev/null +++ b/subprojects/hydra/root/build-step.tt @@ -0,0 +1,61 @@ +[% + buildUrl = c.uri_for('/build', build.id); +%] +[% WRAPPER layout.tt + title="Step ${step.stepnr} of build ${build.id} of job " _ makeNameTextForJob(build.jobset, job) + titleHTML="Step ${step.stepnr}" + _ " of build ${build.id}" + _ " of job " _ linkToJob(build.jobset, job) +%] +[% PROCESS common.tt %] +[% PROCESS "build-common.tt" %] +[% USE HTML %] + +[% logUrl = c.uri_for('/build' build.id 'step' step.stepnr 'log') %] +[% has_log = buildStepLogExists(step) %] + + + + + + + + + + + + + + + [% IF step.system %] + + + + + [% END %] + + + + + + + + + + + + + [% IF has_log %] + + + + + [% END %] +
Type:[% IF step.type == 0 %]Build[% ELSE %]Substitution[% END %]
Derivation:[% step.drvpath | html %]
Output store paths:[% INCLUDE renderOutputsTable outputs=step.buildstepoutputs %]
System:[% step.system | html %]
Machine:[% INCLUDE renderStepMachine %]
Duration:[% INCLUDE renderStepDuration %]
Status: + [% INCLUDE renderStepStatus %] + [% IF step.propagatedfrom %] + (propagated from [% INCLUDE renderBuildIdLink id=step.propagatedfrom.get_column('id') %]) + [% END %] +
Logfile:[% INCLUDE renderLogButtons url=logUrl %]
+ +[% END %] diff --git a/subprojects/hydra/root/build.tt b/subprojects/hydra/root/build.tt index 280a3a705..3263b1101 100644 --- a/subprojects/hydra/root/build.tt +++ b/subprojects/hydra/root/build.tt @@ -30,9 +30,10 @@ END; [% IF ( type == "All" ) || ( type == "Failed" && step.busy == 0 && step.status != 0 ) || ( type == "Running" && step.busy != 0 ) %] [% has_log = seen.${step.drvpath} ? 0 : buildStepLogExists(step); seen.${step.drvpath} = 1; - log = c.uri_for('/build' build.id 'nixlog' step.stepnr); %] + stepUrl = c.uri_for('/build' build.id 'step' step.stepnr); + logUrl = c.uri_for('/build' build.id 'step' step.stepnr 'log'); %] - [% HTML.escape(step.stepnr) %] + stepUrl) %]>[% HTML.escape(step.stepnr) %] [% IF step.type == 0 %] Build of [% INCLUDE renderOutputs outputs=step.buildstepoutputs %] @@ -44,7 +45,7 @@ END; [% INCLUDE renderStepMachine %] [% INCLUDE renderStepStatus %] - [%%] [%+ IF has_log; INCLUDE renderLogLinks url=log inRow=1; END %] + [%%] [%+ IF has_log; INCLUDE renderLogLinks url=logUrl; END %] [%+ IF step.propagatedfrom; %](propagated from [% INCLUDE renderBuildIdLink id=step.propagatedfrom.get_column('id') %])[% END %] @@ -177,7 +178,7 @@ END; Logfile: - [% actualLog = cachedBuildStep ? c.uri_for('/build' cachedBuild.id 'nixlog' cachedBuildStep.stepnr) : c.uri_for('/build' build.id 'log') %] + [% actualLog = cachedBuildStep ? c.uri_for('/build' cachedBuild.id 'step' cachedBuildStep.stepnr 'log') : c.uri_for('/build' build.id 'log') %] [% INCLUDE renderLogButtons url=actualLog %] diff --git a/subprojects/hydra/root/deps.tt b/subprojects/hydra/root/deps.tt index 4cb49af49..91a08335d 100644 --- a/subprojects/hydra/root/deps.tt +++ b/subprojects/hydra/root/deps.tt @@ -13,7 +13,7 @@ [% IF node.buildStep %] c.uri_for('/build' node.buildStep.get_column('build'))) %]>[% node.name %] [% IF buildStepLogExists(node.buildStep); - INCLUDE renderLogLinks url=c.uri_for('/build' node.buildStep.get_column('build') 'nixlog' node.buildStep.stepnr); + INCLUDE renderLogLinks url=c.uri_for('/build' node.buildStep.get_column('build') 'step' node.buildStep.stepnr 'log'); END %] [% ELSE %] [% node.name | html %] (no info) diff --git a/subprojects/hydra/root/log.tt b/subprojects/hydra/root/log.tt index 999c849e0..164a964c7 100644 --- a/subprojects/hydra/root/log.tt +++ b/subprojects/hydra/root/log.tt @@ -1,10 +1,11 @@ [% buildUrl = c.uri_for('/build', build.id); IF step; - logUrl = c.uri_for('/build', build.id, 'nixlog', step.stepnr); + stepUrl = c.uri_for('/build', build.id, 'step', step.stepnr); + logUrl = c.uri_for('/build', build.id, 'step', step.stepnr, 'log'); drvpath = step.drvpath; titleText = "Log of step ${step.stepnr} of build ${build.id} of job " _ makeNameTextForJob(build.jobset, job); - titleLink = "Log of step ${step.stepnr}" + titleLink = "Log of step ${step.stepnr}" _ " of build ${build.id}" _ " of job " _ linkToJob(build.jobset, job); ELSE; diff --git a/subprojects/hydra/root/machine-status.tt b/subprojects/hydra/root/machine-status.tt index 86713c85d..ae8dcffa6 100644 --- a/subprojects/hydra/root/machine-status.tt +++ b/subprojects/hydra/root/machine-status.tt @@ -77,7 +77,7 @@ [% INCLUDE renderFullJobName project=step.project jobset=step.jobset job=step.job %] c.uri_for('/build' step.build)) %]>[% HTML.escape(step.build) %] - [% IF step.busy >= 30 %] c.uri_for('/build' step.build 'nixlog' step.stepnr 'tail')) %]>[% HTML.escape(step.stepnr) %][% ELSE; HTML.escape(step.stepnr); END %] + [% IF step.busy >= 30 %] c.uri_for('/build' step.build 'step' step.stepnr 'log' 'tail')) %]>[% HTML.escape(step.stepnr) %][% ELSE; HTML.escape(step.stepnr); END %] [% step.drvpath.match('-(.*)').0 | html %] [% INCLUDE renderBusyStatus %] [% INCLUDE renderDuration duration = curTime - step.starttime %] diff --git a/subprojects/hydra/root/steps.tt b/subprojects/hydra/root/steps.tt index e53a41b70..703226a65 100644 --- a/subprojects/hydra/root/steps.tt +++ b/subprojects/hydra/root/steps.tt @@ -25,7 +25,7 @@ order of descending finish time.

[% step.drvpath.match('-(.*).drv').0 %] [% INCLUDE renderFullJobNameOfBuild build=step.build %] c.uri_for('/build' step.build.id)) %]>[% HTML.escape(step.build.id) %] - c.uri_for('/build' step.build.id 'nixlog' step.stepnr 'tail')) %]>[% HTML.escape(step.stepnr) %] + c.uri_for('/build' step.build.id 'step' step.stepnr 'log' 'tail')) %]>[% HTML.escape(step.stepnr) %] [% INCLUDE renderRelativeDate timestamp=step.stoptime %] [% INCLUDE renderDuration duration = step.stoptime - step.starttime %] [% INCLUDE renderMachineName machine=step.machine %] From 7c69e41f4299ca4570e86459539c7d71375b1628 Mon Sep 17 00:00:00 2001 From: Amaan Qureshi Date: Mon, 30 Mar 2026 15:38:13 -0400 Subject: [PATCH 6/6] queue-runner: resolve CA derivations at dispatch time Instead of resolving at StepInfo construction and carrying two drv identities through the gRPC layer, resolve in realise_drv_on_valid_machine once all deps are built. If resolution yields a different drv, the original step is marked Resolved and a new DB step is created for the resolved drv with a resolvedTo FK linking them. The builder only ever sees one drv. We create a new Step for that resolution and bunt it back to the scheduler. This grants us more flexibility in execution and the method can be used in the future for dynamic derivations, which won't map 1:1 with the original derivations. We also now only create database steps when we are sure they are necessary, reducing the number of duplicate `BuildStep` rows. In order to make tests more consistent, CA derivations will fail if they cannot be fully resolved. Otherwise, there could be inconsistent successes depending on which builder a step was performed on. As part of this, add local outputs to resolution table With the current queue-runner design, all dependency outputs of a CAFloating derivation must be recorded in the hydra database. This is true for things built or substituted by hydra, but until now not by things found on the local nix store. This may occur for outputs that are part of the system configuration. Therefore, add all local outputs that are not already in the database to the resolution table upon creating a step. This makes it possible to build derivations from `contentAddressedByDefault` nixpkgs. Co-Authored-By: Artemis Tosini Co-Authored-By: John Ericson Co-Authored-By: Amaan Qureshi --- subprojects/crates/db/src/connection.rs | 236 +++++++++++++++- subprojects/crates/db/src/models.rs | 3 + subprojects/hydra-builder/src/state.rs | 10 +- .../hydra-queue-runner/src/state/build.rs | 2 +- .../hydra-queue-runner/src/state/drv.rs | 44 +++ .../hydra-queue-runner/src/state/machine.rs | 15 +- .../hydra-queue-runner/src/state/mod.rs | 261 ++++++++++++++++-- .../hydra-queue-runner/src/state/queue.rs | 1 + .../hydra-queue-runner/src/state/step.rs | 22 ++ .../hydra-queue-runner/src/state/step_info.rs | 60 +--- subprojects/hydra-queue-runner/src/utils.rs | 28 ++ .../hydra-tests/content-addressed/basic.t | 30 +- .../hydra-tests/content-addressed/dyn-drv.t | 4 + .../lib/Hydra/Schema/Result/BuildSteps.pm | 12 +- subprojects/hydra/sql/hydra.sql | 10 + subprojects/hydra/sql/upgrade-86.sql | 1 + subprojects/proto/v1/streaming.proto | 2 +- 17 files changed, 637 insertions(+), 104 deletions(-) create mode 100644 subprojects/hydra-queue-runner/src/state/drv.rs create mode 100644 subprojects/hydra/sql/upgrade-86.sql diff --git a/subprojects/crates/db/src/connection.rs b/subprojects/crates/db/src/connection.rs index 77f48e8dc..6fb27d830 100644 --- a/subprojects/crates/db/src/connection.rs +++ b/subprojects/crates/db/src/connection.rs @@ -355,12 +355,20 @@ impl Connection { CROSS JOIN LATERAL ( SELECT o.path FROM buildsteps s + -- If this step was resolved, look up outputs from + -- the resolved drv's successful buildstep instead. + -- resolvedDrvPath is a bare basename; drvPath is a + -- full path, so strip the directory prefix to compare. + LEFT JOIN buildsteps sr + ON sr.drvPath = $2 || '/' || s.resolvedDrvPath + AND sr.status = 0 JOIN buildstepoutputs o - ON s.build = o.build AND s.stepnr = o.stepnr + ON o.build = COALESCE(sr.build, s.build) + AND o.stepnr = COALESCE(sr.stepnr, s.stepnr) WHERE s.drvPath = r.drv_path AND o.name = i.chain[r.step] AND o.path IS NOT NULL - AND s.status = 0 + AND (s.status = 0 OR s.status = 13) ORDER BY s.build DESC LIMIT 1 ) sub @@ -376,6 +384,7 @@ impl Connection { ", ) .bind(&json_input) + .bind(store_dir.to_str()) .fetch_all(&mut *self.conn) .await?; @@ -693,6 +702,36 @@ impl Transaction<'_> { Ok(()) } + #[tracing::instrument(skip(self), err)] + pub async fn find_build_step_outputs( + &mut self, + store_dir: &StoreDir, + drv_path: &StorePath, + ) -> sqlx::Result> { + let drv_path = store_dir.display(drv_path).to_string(); + let items: Vec<(String, String)> = sqlx::query_as( + r"SELECT o.name, o.path + FROM buildstepoutputs o + JOIN buildsteps s ON s.build = o.build AND s.stepnr = o.stepnr + WHERE s.drvpath = $1 AND o.path IS NOT NULL", + ) + .bind(drv_path) + .fetch_all(&mut *self.tx) + .await?; + + items + .into_iter() + .map(|(name, path)| -> anyhow::Result<_> { + let name: OutputName = name.parse().context("invalid output name from DB")?; + let path: StorePath = store_dir + .parse(&path) + .context("invalid store path from DB")?; + Ok((name, path)) + }) + .collect::>() + .map_err(|e| sqlx::Error::Decode(e.into_boxed_dyn_error())) + } + #[tracing::instrument(skip(self, res), err)] pub async fn update_build_step_in_finish( &mut self, @@ -941,6 +980,83 @@ impl Transaction<'_> { Ok(step_nr) } + /// Record which resolved drv path this step was resolved to. + #[tracing::instrument(skip(self), err)] + pub async fn set_resolved_to( + &mut self, + origin_build_id: crate::models::BuildID, + origin_step_nr: i32, + resolved_drv_path: &StorePath, + ) -> sqlx::Result<()> { + sqlx::query( + r" + UPDATE buildsteps + SET resolvedDrvPath = $3 + WHERE build = $1 AND stepnr = $2 + ", + ) + .bind(origin_build_id) + .bind(origin_step_nr) + .bind(resolved_drv_path.to_string()) + .execute(&mut *self.tx) + .await?; + Ok(()) + } + + #[tracing::instrument( + skip(self, start_time, stop_time, build_id, drv_path, outputs,), + err, + ret + )] + pub async fn create_local_step( + &mut self, + store_dir: &StoreDir, + start_time: i32, + stop_time: i32, + build_id: crate::models::BuildID, + drv_path: &StorePath, + outputs: BTreeMap, + ) -> anyhow::Result { + let step_nr = loop { + if let Some(step_nr) = self + .insert_build_step( + store_dir, + InsertBuildStep { + build_id, + r#type: crate::models::BuildType::Substitution, + drv_path, + status: BuildStatus::Success, + busy: false, + start_time: Some(start_time), + stop_time: Some(stop_time), + platform: None, + propagated_from: None, + error_msg: None, + machine: "", + }, + ) + .await? + { + break step_nr; + } + }; + + let output_items: Vec<_> = outputs + .into_iter() + .map(|(name, path)| InsertBuildStepOutput { + build_id, + step_nr, + name, + path: Some(path), + }) + .collect(); + + self.insert_build_step_outputs(store_dir, &output_items) + .await?; + + Ok(step_nr) + } + #[tracing::instrument( skip(self, start_time, stop_time, build_id, drv_path, output,), err, @@ -1191,11 +1307,24 @@ mod tests { } async fn insert_step(conn: &mut Connection, build: i32, stepnr: i32, drv_path: &StorePath) { + insert_step_with_status(conn, build, stepnr, drv_path, 0, None).await; + } + + async fn insert_step_with_status( + conn: &mut Connection, + build: i32, + stepnr: i32, + drv_path: &StorePath, + status: i32, + resolved_drv_path: Option<&StorePath>, + ) { let sd = test_store_dir(); - sqlx::query("INSERT INTO BuildSteps (build, stepnr, type, busy, drvPath, status) VALUES ($1, $2, 0, 0, $3, 0)") + sqlx::query("INSERT INTO BuildSteps (build, stepnr, type, busy, drvPath, status, resolvedDrvPath) VALUES ($1, $2, 0, 0, $3, $4, $5)") .bind(build) .bind(stepnr) .bind(sd.display(drv_path).to_string()) + .bind(status) + .bind(resolved_drv_path.map(|p| p.to_string())) .execute(&mut *conn.conn) .await .unwrap(); @@ -1333,6 +1462,70 @@ mod tests { ); } + /// A step that was resolved (status=13) with `resolvedDrvPath` pointing + /// to a different drv whose successful buildstep has the outputs. + #[tokio::test] + async fn resolve_through_resolved_step() { + let (_pg, mut conn) = setup().await; + + // Step 1: unresolved ca-depending-on-ca.drv, status=Resolved(13), + // resolvedDrvPath points to the resolved drv + insert_step_with_status( + &mut conn, + 1, + 1, + &sp("unresolved.drv"), + 13, + Some(&sp("resolved.drv")), + ) + .await; + // A successful buildstep for the resolved drv (could be any build) + insert_step(&mut conn, 2, 1, &sp("resolved.drv")).await; + insert_output(&mut conn, 2, 1, "out", &sp("result")).await; + + // Looking up via the unresolved drv path should find the output + // through the resolvedDrvPath chain. + let results = conn + .resolve_drv_output_chains(&test_store_dir(), &[(&sp("unresolved.drv"), &[&on("out")])]) + .await + .unwrap(); + assert_eq!(results, vec![Some(sp("result"))]); + } + + /// A depth-2 chain where the first step was resolved: + /// unresolved.drv (status=13, resolvedDrvPath→resolved.drv) → + /// resolved.drv outputs a .drv path → that .drv has the final output. + #[tokio::test] + async fn resolve_depth_2_through_resolved_step() { + let (_pg, mut conn) = setup().await; + + // Build 1: unresolved step, resolved to bbb-resolved.drv + insert_step_with_status( + &mut conn, + 1, + 1, + &sp("unresolved.drv"), + 13, + Some(&sp("resolved.drv")), + ) + .await; + insert_step(&mut conn, 2, 1, &sp("resolved.drv")).await; + insert_output(&mut conn, 2, 1, "out", &sp("intermediate.drv")).await; + + // Build 3: the intermediate drv + insert_step(&mut conn, 3, 1, &sp("intermediate.drv")).await; + insert_output(&mut conn, 3, 1, "out", &sp("final")).await; + + let results = conn + .resolve_drv_output_chains( + &test_store_dir(), + &[(&sp("unresolved.drv"), &[&on("out"), &on("out")])], + ) + .await + .unwrap(); + assert_eq!(results, vec![Some(sp("final"))]); + } + /// Batch with ragged depths: one depth-1 (Opaque), one depth-2 (Built), /// one depth-3 (Built(Built(...))). #[tokio::test] @@ -1377,4 +1570,41 @@ mod tests { ] ); } + + /// Depth-1 lookup where the only buildstep for the drv has + /// status=Resolved(13) with `resolvedDrvPath` pointing to + /// a different drv whose successful buildstep has the outputs. + /// This matches the production scenario: ca-depending-on-ca.drv + /// was resolved to a different drv, and ca-depending-on-ca- + /// depending-on-ca needs to look up its output. + #[tokio::test] + async fn resolve_depth_1_via_resolved_step() { + let (_pg, mut conn) = setup().await; + + // Build 1, step 1: unresolved ca-depending-on-ca.drv + // status=13 (Resolved), resolvedDrvPath points to the resolved drv + insert_step_with_status( + &mut conn, + 1, + 1, + &sp("unresolved-ca-dep.drv"), + 13, + Some(&sp("resolved-ca-dep.drv")), + ) + .await; + // Build 2: the resolved drv was built successfully + insert_step(&mut conn, 2, 1, &sp("resolved-ca-dep.drv")).await; + insert_output(&mut conn, 2, 1, "out", &sp("ca-dep-output")).await; + + // Depth-1 chain: look up "out" of the unresolved drv path. + // The query should follow resolvedDrvPath to find the output. + let results = conn + .resolve_drv_output_chains( + &test_store_dir(), + &[(&sp("unresolved-ca-dep.drv"), &[&on("out")])], + ) + .await + .unwrap(); + assert_eq!(results, vec![Some(sp("ca-dep-output"))]); + } } diff --git a/subprojects/crates/db/src/models.rs b/subprojects/crates/db/src/models.rs index 2d6838246..776ee05ae 100644 --- a/subprojects/crates/db/src/models.rs +++ b/subprojects/crates/db/src/models.rs @@ -22,6 +22,8 @@ pub enum BuildStatus { LogLimitExceeded = 10, NarSizeLimitExceeded = 11, NotDeterministic = 12, + /// step was resolved to a CA derivation, see resolvedTo FK + Resolved = 13, /// not stored Busy = 100, } @@ -42,6 +44,7 @@ impl BuildStatus { 10 => Some(Self::LogLimitExceeded), 11 => Some(Self::NarSizeLimitExceeded), 12 => Some(Self::NotDeterministic), + 13 => Some(Self::Resolved), 100 => Some(Self::Busy), _ => None, } diff --git a/subprojects/hydra-builder/src/state.rs b/subprojects/hydra-builder/src/state.rs index 002798d6c..caade2538 100644 --- a/subprojects/hydra-builder/src/state.rs +++ b/subprojects/hydra-builder/src/state.rs @@ -408,8 +408,6 @@ impl State { .drv .ok_or(JobFailure::Preparing(anyhow::anyhow!("missing drv")))? .0; - let resolved_drv = m.resolved_drv.map(|v| v.0); - let maybe_resolved_drv = resolved_drv.as_ref().unwrap_or(&drv); let before_import = Instant::now(); let gcroot_prefix = uuid::Uuid::new_v4().to_string(); @@ -427,7 +425,7 @@ impl State { .await; let requisites = client .fetch_drv_requisites(FetchRequisitesRequest { - path: Some(ProtoStorePath::from(maybe_resolved_drv.clone())), + path: Some(ProtoStorePath::from(drv.clone())), include_outputs: false, }) .await @@ -440,7 +438,7 @@ impl State { store.clone(), self.metrics.clone(), &gcroot, - maybe_resolved_drv, + &drv, requisites.into_iter().map(|s| s.0), usize::try_from(self.max_concurrent_downloads.load(Ordering::Relaxed)).unwrap_or(5), self.config.use_substitutes, @@ -459,7 +457,7 @@ impl State { let before_build = Instant::now(); let (mut child, stdout, mut stderr) = nix_utils::realise_drv( &store, - maybe_resolved_drv, + &drv, &nix_utils::BuildOptions::complete(m.max_log_size, m.max_silent_time, m.build_timeout), true, ) @@ -536,7 +534,7 @@ impl State { .store_dir() .parse(&output_raw[0].drv_path) .map_err(|e: nix_utils::ParseStorePathError| JobFailure::PostProcessing(e.into()))?; - if &actual_out_drv != maybe_resolved_drv { + if actual_out_drv != drv { return Err(JobFailure::PostProcessing(anyhow::anyhow!( "Nix returned outputs for {actual_out_drv} when we expected {drv}" ))); diff --git a/subprojects/hydra-queue-runner/src/state/build.rs b/subprojects/hydra-queue-runner/src/state/build.rs index 65f56392b..0f13211ad 100644 --- a/subprojects/hydra-queue-runner/src/state/build.rs +++ b/subprojects/hydra-queue-runner/src/state/build.rs @@ -25,7 +25,7 @@ pub struct Build { pub local_priority: i32, pub global_priority: AtomicI32, - toplevel: arc_swap::ArcSwapOption, + pub toplevel: arc_swap::ArcSwapOption, pub jobset: Arc, finished_in_db: AtomicBool, diff --git a/subprojects/hydra-queue-runner/src/state/drv.rs b/subprojects/hydra-queue-runner/src/state/drv.rs new file mode 100644 index 000000000..97af92693 --- /dev/null +++ b/subprojects/hydra-queue-runner/src/state/drv.rs @@ -0,0 +1,44 @@ +use nix_utils::SingleDerivedPath; + +/// Output names of intermediate derivations for a dynamic derivation +/// dependency, stored in reverse order so that the next level to resolve +/// can be cheaply `pop()`-ed. +/// +/// e.g. for a derivation input that is `aaaa-dyn.drv^foo^bar^out`, the final +/// derivation would be `aaaa-dyn.drv^foo^bar` (no final `^out`). The output +/// names are stored as `["bar", "foo"]` (reversed). +/// +/// In the common case of depending on a static derivation, this is empty. +#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord)] +pub struct OutputNameChain(pub Vec); + +impl OutputNameChain { + pub fn pop(&mut self) -> Option { + self.0.pop() + } +} + +/// Flatten a [`SingleDerivedPath`] into `(root_drv_path, chain)`. +/// +/// The output chain is in stack order (outermost first) matching +/// [`OutputNameChain`]'s convention. For `Built { Opaque(A), "foo" }`, +/// returns `(A, ["foo"])`. For `Opaque(A)`, returns `(A, [])`. +pub fn flatten_path(sdp: &SingleDerivedPath) -> (nix_utils::StorePath, OutputNameChain) { + match sdp { + SingleDerivedPath::Opaque(p) => (p.clone(), OutputNameChain::default()), + SingleDerivedPath::Built { drv_path, output } => flatten_chain(drv_path, output), + } +} + +/// Like [`flatten_path`] but appends an additional output name. +/// +/// For `Built { Opaque(A), "foo" }` with output `"bar"`, +/// returns `(A, ["bar", "foo"])`. +pub fn flatten_chain( + drv_path: &SingleDerivedPath, + output_name: &nix_utils::OutputName, +) -> (nix_utils::StorePath, OutputNameChain) { + let (root, mut chain) = flatten_path(drv_path); + chain.0.push(output_name.clone()); + (root, chain) +} diff --git a/subprojects/hydra-queue-runner/src/state/machine.rs b/subprojects/hydra-queue-runner/src/state/machine.rs index 9ab4fc89c..1851902fd 100644 --- a/subprojects/hydra-queue-runner/src/state/machine.rs +++ b/subprojects/hydra-queue-runner/src/state/machine.rs @@ -491,22 +491,16 @@ impl Machines { pub struct Job { pub internal_build_id: uuid::Uuid, pub path: nix_utils::StorePath, - pub resolved_drv: Option, pub build_id: BuildID, pub step_nr: i32, pub result: RemoteBuild, } impl Job { - pub fn new( - build_id: BuildID, - path: nix_utils::StorePath, - resolved_drv: Option, - ) -> Self { + pub fn new(build_id: BuildID, path: nix_utils::StorePath) -> Self { Self { internal_build_id: uuid::Uuid::new_v4(), path, - resolved_drv, build_id, step_nr: 0, result: RemoteBuild::new(), @@ -538,7 +532,6 @@ pub enum Message { BuildMessage { build_id: uuid::Uuid, drv: nix_utils::StorePath, - resolved_drv: Option, max_log_size: u64, max_silent_time: i32, build_timeout: i32, @@ -560,7 +553,6 @@ impl Message { Self::BuildMessage { build_id, drv, - resolved_drv, max_log_size, max_silent_time, build_timeout, @@ -568,7 +560,6 @@ impl Message { } => runner_request::Message::Build(BuildMessage { build_id: build_id.to_string(), drv: Some(shared::proto::ProtoStorePath::from(drv)), - resolved_drv: resolved_drv.map(shared::proto::ProtoStorePath::from), max_log_size, max_silent_time, build_timeout, @@ -696,15 +687,15 @@ impl Machine { pub async fn build_drv( &self, job: Job, + effective_drv: nix_utils::StorePath, opts: &nix_utils::BuildOptions, presigned_url_opts: Option, ) -> anyhow::Result<()> { - let drv = job.path.clone(); + let drv = effective_drv; self.msg_queue .send(Message::BuildMessage { build_id: job.internal_build_id, drv, - resolved_drv: job.resolved_drv.clone(), max_log_size: opts.get_max_log_size(), max_silent_time: opts.get_max_silent_time(), build_timeout: opts.get_build_timeout(), diff --git a/subprojects/hydra-queue-runner/src/state/mod.rs b/subprojects/hydra-queue-runner/src/state/mod.rs index c62128fa4..68cf65e5c 100644 --- a/subprojects/hydra-queue-runner/src/state/mod.rs +++ b/subprojects/hydra-queue-runner/src/state/mod.rs @@ -1,5 +1,6 @@ mod atomic; mod build; +pub mod drv; mod fod_checker; mod inspectable_channel; mod jobset; @@ -10,6 +11,7 @@ mod step; mod step_info; mod uploader; +use anyhow::Context as _; pub use atomic::AtomicDateTime; pub use build::{Build, BuildOutput, BuildResultState, BuildTimings, Builds, RemoteBuild}; pub use jobset::{Jobset, JobsetID, Jobsets}; @@ -18,6 +20,7 @@ pub use queue::{BuildQueueStats, Queues}; pub use step::{Step, Steps}; pub use step_info::StepInfo; +use std::collections::BTreeMap; use std::sync::Arc; use std::sync::atomic::{AtomicI64, Ordering}; use std::time::Instant; @@ -46,6 +49,8 @@ enum CreateStepResult { enum RealiseStepResult { None, + /// Created a new resolved `BuildStep` + Resolved, Valid(Arc), MaybeCancelled, CachedFailure, @@ -275,7 +280,7 @@ impl State { let drv = step_info.step.get_drv_path(); let mut build_options = nix_utils::BuildOptions::new(None); - let build_id = { + let build = { let mut dependents = HashSet::new(); let mut steps = HashSet::new(); step_info.step.get_dependents(&mut dependents, &mut steps); @@ -308,14 +313,12 @@ impl State { build_options .set_max_silent_time(biggest_max_silent_time.unwrap_or(build.max_silent_time)); build_options.set_build_timeout(biggest_build_timeout.unwrap_or(build.timeout)); - build.id + build.clone() }; - let mut job = machine::Job::new( - build_id, - drv.to_owned(), - step_info.resolved_drv_path.clone(), - ); + let build_id = build.id; + + let mut job = machine::Job::new(build_id, drv.to_owned()); job.result.set_start_time_now(); if self.check_cached_failure(step_info.step.clone()).await { job.result.step_status = BuildStatus::CachedFailure; @@ -352,33 +355,209 @@ impl State { .collect(), ) .await?; + tx.commit().await?; step_nr }; job.step_nr = step_nr; + // Try to resolve CA derivation inputs. If resolution yields a + // different drv, mark this step as Resolved in the DB and create a new + // step for the resolved drv that will be built at a later time. + { + let Some(guard) = step_info.step.get_drv() else { + // Should never happen + return Ok(RealiseStepResult::MaybeCancelled); + }; + let drv_ref = guard.as_ref().unwrap(); + + // `Some(path)` if this CA derivation was resolved to a + // different drv; `None` if resolution is not needed or is + // a no-op (same drv path). + let resolved_path = async { + // Only CA floating derivations need resolution, and only + // when they have `Built` inputs (dynamic derivation deps). + // `Opaque`-only inputs are already resolved. + let is_ca_with_built_inputs = + drv_ref.outputs.iter().all(|output| { + matches!(output.1, nix_utils::DerivationOutput::CAFloating(_)) + }) && drv_ref + .inputs + .iter() + .any(|input| matches!(input, nix_utils::SingleDerivedPath::Built { .. })); + tracing::info!( + is_ca_with_built_inputs, + ca_floating = drv_ref + .outputs + .iter() + .all(|o| matches!(o.1, nix_utils::DerivationOutput::CAFloating(_))), + has_built_inputs = drv_ref + .inputs + .iter() + .any(|i| matches!(i, nix_utils::SingleDerivedPath::Built { .. })), + "resolution check for {drv}" + ); + if !is_ca_with_built_inputs { + return Ok::<_, anyhow::Error>(None); + } + + // Resolve `Built` input placeholders to concrete store + // paths using outputs recorded in the DB. + let basic_drv = StepInfo::try_resolve(self.store.store_dir(), &self.db, drv_ref) + .await + .ok_or_else(|| { + anyhow::anyhow!("Failed to resolve CAFloating derivation {drv}") + })?; + let resolved_path = self.store.write_derivation(&basic_drv).await?; + // If resolution changed the drv, we need a two-phase + // build; otherwise just build the original directly. + Ok((&resolved_path != drv).then_some(resolved_path)) + } + .await?; + + if let Some(resolved_path) = resolved_path { + tracing::info!("resolved CA derivation {drv} -> {resolved_path}"); + + // Record the resolved drv path on the original step so + // future output lookups can follow the chain. + let mut tx = db.begin_transaction().await?; + tx.set_resolved_to(build_id, step_nr, &resolved_path) + .await?; + tx.commit().await?; + + // Finish original step as "resolved" in the DB and in-memory + step_info.step.set_finished(true); + let mut resolved_result = RemoteBuild::new(); + resolved_result.step_status = BuildStatus::Resolved; + resolved_result.set_start_time_now(); + resolved_result.set_stop_time_now(); + resolved_result.log_file.clone_from(&job.result.log_file); + finish_build_step( + &self.db, + &self.store, + build_id, + step_nr, + &resolved_result, + Some(&machine.hostname), + None, + ) + .await?; + + // Create a resolved in-memory step + // We do not need the global state because they are only relevant when making + // multiple steps. Our resolved step, by definition, has no dependencies, so + // only one step will ever be created. + // finished_drvs: A list of previously created steps within a build. + // Without this, it is possible that we will make a duplicate step if two different + // steps in the same build resolve to the same derivation, but it will not cause + // problems. + // new_steps: An output list of all created steps. Since only one is being made, the root, + // we can take the return of the function. + // new_runnable: An output list of the runnable steps. Again, only one will be made, so we + // can take the function return. + // Only mark the resolved step as a direct build if the + // unresolved step was the toplevel derivation of the build. + // Otherwise, `succeed_step` would prematurely mark the build as + // finished when an intermediate resolved step completes. + let is_toplevel = build.toplevel.load().as_deref() == Some(&*step_info.step); + let referring_build = if is_toplevel { + Some(build.clone()) + } else { + None + }; + let resolved_step = match self + .create_step( + build.clone(), + resolved_path, + referring_build, + None, + Arc::new(parking_lot::RwLock::new(HashSet::new())), + Arc::new(parking_lot::RwLock::new(HashSet::new())), + Arc::new(parking_lot::RwLock::new(HashSet::new())), + ) + .await + { + CreateStepResult::None => { + return Err(anyhow::anyhow!("Could not create resolved build step")); + } + CreateStepResult::Valid(step) => step, + CreateStepResult::PreviousFailure(step) => { + self.handle_previous_failure(build.clone(), step.clone()) + .await + .with_context(|| { + format!( + "Failed to handle previous failure in resolved version of {drv}" + ) + })?; + return Ok(RealiseStepResult::CachedFailure); + } + }; + + resolved_step.make_runnable(); + + // The in-memory `Arc` objects are kept alive by having a reference + // from a `Build` (if they are the root build) or a dependant `Step` + // (if they are not). + // Therefore, we must prevent our new resolved step from being garbage + // collected by marking it as a dependency of the old step's reverse + // dependencies and, if it was the root derivation, as the new root + // derivation of the build. + + // Replace the original step with the resolved step in the + // dependency graph. Each step that depended on the original + // (unresolved) step must now depend on the resolved step + // instead, otherwise it will never become runnable (the + // original step's drv path differs from the resolved one, so + // completing the resolved step wouldn't clear the dep). + for rdep in step_info.step.clone_rdeps() { + if let Some(rdep) = rdep.upgrade() { + rdep.remove_dep(&step_info.step); + resolved_step.make_rdep(&rdep); + } + } + + // Make the resolved step the new root of the build if the old + // unresolved step was previously the root. + if *build + .toplevel + .compare_and_swap(&step_info.step, Some(resolved_step)) + == Some(step_info.step.clone()) + { + build.propagate_priorities(); + } + + // New steps runnable + self.trigger_dispatch(); + + // No more work to do, build will happen in another step + return Ok(RealiseStepResult::Resolved); + } + } + { let mut tx = db.begin_transaction().await?; tx.notify_build_started(build_id).await?; tx.commit().await?; } tracing::info!( - "Submitting build drv={drv} on machine={} hostname={} build_id={build_id} step_nr={step_nr}", + "Submitting build drv={drv} on machine={} hostname={} build_id={build_id} step_nr={}", machine.id, - machine.hostname + machine.hostname, + job.step_nr, ); self.db .get() .await? .update_build_step(db::models::UpdateBuildStep { build_id, - step_nr, + step_nr: job.step_nr, status: db::models::StepStatus::Connecting, }) .await?; machine .build_drv( job, + drv.clone(), &build_options, // TODO: cleanup if self.config.use_presigned_uploads() { @@ -952,7 +1131,7 @@ impl State { if r.atomic_state.tries.load(Ordering::Relaxed) > 0 { continue; } - let step_info = StepInfo::new(&self.store, &self.db, r.clone()).await; + let step_info = StepInfo::new(r.clone()); new_queues .entry(system) @@ -1191,7 +1370,7 @@ impl State { { let mut db = self.db.get().await?; let mut tx = db.begin_transaction().await?; - for b in direct { + for b in &direct { tx.notify_build_finished(b.id, &[]).await?; } @@ -1771,6 +1950,7 @@ impl State { .ok() .flatten() else { + tracing::warn!("create_step: could not query derivation {drv_path}, skipping"); return CreateStepResult::None; }; if let Some(fod_checker) = &self.fod_checker { @@ -1796,17 +1976,45 @@ impl State { }) }; let output_paths = nix_utils::output_paths(&drv, self.store.store_dir()); + let known_outputs = self + .query_known_drv_outputs(&drv_path) + .await + .unwrap_or_else(|e| { + tracing::warn!("Could not query known outputs, continuing: {e}"); + BTreeMap::new() + }); + let missing_local_outputs = self.store.query_missing_outputs(output_paths.clone()).await; + // Handle paths that aren't in the database (for resolution) + // existing_local_outputs = output_paths - missing_local_outputs + // unregistered_local_outputs = existing_local_outputs - known_outputs + let unregistered_local_outputs = output_paths + .iter() + .filter(|(name, path)| { + path.is_some() + && !missing_local_outputs.contains_key(name) + && !known_outputs.contains_key(name) + }) + .map(|(name, path)| (name.clone(), path.clone())) + .collect::>(); + if !unregistered_local_outputs.is_empty() + && let Err(e) = crate::utils::make_local_step( + &self.db, + &self.store, + build.id, + &drv_path, + &unregistered_local_outputs, + ) + .await + { + tracing::warn!("Failed to mark outputs as already found, continuing: {e}"); + } + + // Handle paths that aren't in the remote store (for pushing) let missing_outputs = if let Some(ref remote_store) = remote_store { let mut missing = remote_store .query_missing_remote_outputs(output_paths.clone()) .await; - if !missing.is_empty() - && self - .store - .query_missing_outputs(output_paths.clone()) - .await - .is_empty() - { + if !missing.is_empty() && missing_local_outputs.is_empty() { // we have all paths locally, so we can just upload them to the remote_store if let Ok(log_file) = self.construct_log_file_path(&drv_path).await { let missing_paths: Vec = @@ -1872,6 +2080,7 @@ impl State { }; if finished { + tracing::info!("create_step: {drv_path} already finished (outputs in store), skipping"); if let Some(fod_checker) = &self.fod_checker { fod_checker.to_traverse(&drv_path); } @@ -1940,6 +2149,18 @@ impl State { CreateStepResult::Valid(step) } + #[tracing::instrument(skip(self))] + async fn query_known_drv_outputs( + &self, + drv_path: &nix_utils::StorePath, + ) -> anyhow::Result> { + let mut db = self.db.get().await?; + let mut tx = db.begin_transaction().await?; + Ok(tx + .find_build_step_outputs(self.store.store_dir(), drv_path) + .await?) + } + #[tracing::instrument(skip(self, step), ret, level = "debug")] async fn check_cached_failure(&self, step: Arc) -> bool { let Some(drv_outputs) = step.get_output_paths(self.store.store_dir()) else { @@ -2098,7 +2319,7 @@ impl State { continue; }; - let mut job = machine::Job::new(build.id, drv.to_owned(), None); + let mut job = machine::Job::new(build.id, drv.to_owned()); job.result.set_start_and_stop(now); job.result.step_status = BuildStatus::Unsupported; job.result.error_msg = Some(format!( diff --git a/subprojects/hydra-queue-runner/src/state/queue.rs b/subprojects/hydra-queue-runner/src/state/queue.rs index b7ae35a69..ed5917249 100644 --- a/subprojects/hydra-queue-runner/src/state/queue.rs +++ b/subprojects/hydra-queue-runner/src/state/queue.rs @@ -504,6 +504,7 @@ impl Queues { nr_waiting += 1; nr_steps_waiting_all_queues += 1; } + Ok(crate::state::RealiseStepResult::Resolved) => {} Ok( crate::state::RealiseStepResult::MaybeCancelled | crate::state::RealiseStepResult::CachedFailure, diff --git a/subprojects/hydra-queue-runner/src/state/step.rs b/subprojects/hydra-queue-runner/src/state/step.rs index ec8b79c1c..b6e0a087b 100644 --- a/subprojects/hydra-queue-runner/src/state/step.rs +++ b/subprojects/hydra-queue-runner/src/state/step.rs @@ -363,6 +363,28 @@ impl Step { .store(state.deps.len() as u64, Ordering::Relaxed); } + pub fn remove_dep(&self, dep: &Arc) { + let mut state = self.state.write(); + state.deps.remove(dep); + self.atomic_state + .deps_len + .store(state.deps.len() as u64, Ordering::Relaxed); + } + + pub fn make_rdep(self: &Arc, dep: &Arc) { + dep.add_dep(self.clone()); + let mut state = self.state.write(); + state.rdeps.push(Arc::downgrade(dep)); + self.atomic_state + .rdeps_len + .store(state.rdeps.len() as u64, Ordering::Relaxed); + } + + pub fn clone_rdeps(&self) -> Vec> { + let state = self.state.read(); + state.rdeps.clone() + } + pub fn add_referring_data( &self, referring_build: Option<&Arc>, diff --git a/subprojects/hydra-queue-runner/src/state/step_info.rs b/subprojects/hydra-queue-runner/src/state/step_info.rs index 43d80b0a7..1459bcace 100644 --- a/subprojects/hydra-queue-runner/src/state/step_info.rs +++ b/subprojects/hydra-queue-runner/src/state/step_info.rs @@ -2,41 +2,14 @@ use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use db::models::BuildID; -use nix_utils::BaseStore as _; use nix_utils::SingleDerivedPath; use super::Step; - -/// Flatten a [`SingleDerivedPath`] + output name into `(root_drv_path, [outputs...])`. -/// The output chain is in resolution order: for `Built { Opaque(A), "out" }` with -/// final output `"dev"`, returns `(A, ["out", "dev"])`. -fn flatten_chain( - drv_path: &SingleDerivedPath, - output_name: &nix_utils::OutputName, -) -> (nix_utils::StorePath, Vec) { - let mut outputs = Vec::::new(); - let mut current = drv_path; - let root = loop { - match current { - SingleDerivedPath::Opaque(p) => break p.clone(), - SingleDerivedPath::Built { - drv_path: parent, - output, - } => { - outputs.push(output.clone()); - current = parent; - } - } - }; - outputs.reverse(); - outputs.push(output_name.clone()); - (root, outputs) -} +use super::drv::flatten_chain; #[derive(Debug)] pub struct StepInfo { pub step: Arc, - pub resolved_drv_path: Option, already_scheduled: AtomicBool, cancelled: AtomicBool, pub runnable_since: jiff::Timestamp, @@ -44,19 +17,8 @@ pub struct StepInfo { } impl StepInfo { - pub async fn new(store: &nix_utils::LocalStore, db: &db::Database, step: Arc) -> Self { + pub fn new(step: Arc) -> Self { Self { - resolved_drv_path: match step.get_drv() { - Some(guard) => { - let resolved = - Self::try_resolve(store.store_dir(), db, guard.as_ref().unwrap()).await; - match resolved { - Some(ref basic_drv) => store.write_derivation(basic_drv).await.ok(), - None => None, - } - } - None => None, - }, already_scheduled: false.into(), cancelled: false.into(), runnable_since: step.get_runnable_since(), @@ -76,7 +38,7 @@ impl StepInfo { /// /// We only need a store dir, not a store, because all the info we need comes from the Hydra /// database. - async fn try_resolve( + pub(super) async fn try_resolve( store_dir: &nix_utils::StoreDir, db: &db::Database, drv: &nix_utils::Derivation, @@ -114,20 +76,21 @@ impl StepInfo { tokio::task::block_in_place(|| { // Flatten each SingleDerivedPath chain into (root, [outputs...]) // and resolve everything in a single recursive SQL query. - let chains = inputs + let chains: Vec<_> = inputs .iter() .map(|(drv_path, output_name)| flatten_chain(drv_path, output_name)) - .collect::>(); + .collect(); - let chain_refs = chains + // SQL needs forward order; OutputNameChain stores reversed. + let chain_refs: Vec<_> = chains .iter() - .map(|(root, outputs)| (root, outputs.iter().collect::>())) - .collect::>(); + .map(|(root, chain)| (root, chain.0.iter().rev().collect::>())) + .collect(); - let sql_input = chain_refs + let sql_input: Vec<_> = chain_refs .iter() .map(|(root, outputs)| (*root, outputs.as_slice())) - .collect::>(); + .collect(); tokio::runtime::Handle::current() .block_on(conn.resolve_drv_output_chains(store_dir, &sql_input)) @@ -278,7 +241,6 @@ mod tests { StepInfo { step, - resolved_drv_path: None, already_scheduled: false.into(), cancelled: false.into(), runnable_since: jiff::Timestamp::now(), diff --git a/subprojects/hydra-queue-runner/src/utils.rs b/subprojects/hydra-queue-runner/src/utils.rs index 6722647bf..5b7486493 100644 --- a/subprojects/hydra-queue-runner/src/utils.rs +++ b/subprojects/hydra-queue-runner/src/utils.rs @@ -107,3 +107,31 @@ pub async fn substitute_output( Ok(true) } + +#[tracing::instrument(skip(db, store, ), fields(%drv_path), err(level=tracing::Level::WARN))] +pub async fn make_local_step( + db: &db::Database, + store: &nix_utils::LocalStore, + build_id: BuildID, + drv_path: &StorePath, + missing: &BTreeMap>, +) -> anyhow::Result<()> { + let time = i32::try_from(jiff::Timestamp::now().as_second())?; + + let mut db = db.get().await?; + let mut tx = db.begin_transaction().await?; + tx.create_local_step( + store.store_dir(), + time, + time, + build_id, + drv_path, + missing + .iter() + .filter_map(|(name, path)| path.as_ref().map(|p| (name.clone(), p.clone()))) + .collect(), + ) + .await?; + tx.commit().await?; + Ok(()) +} diff --git a/subprojects/hydra-tests/content-addressed/basic.t b/subprojects/hydra-tests/content-addressed/basic.t index 8f3dfeff6..bc5731936 100644 --- a/subprojects/hydra-tests/content-addressed/basic.t +++ b/subprojects/hydra-tests/content-addressed/basic.t @@ -88,16 +88,26 @@ my $downstream2 = $db->resultset('Builds')->find({ my $downstream1_out = $downstream1->buildoutputs->find({ name => "out" }); my $downstream2_out = $downstream2->buildoutputs->find({ name => "out" }); is($downstream1_out->path, $downstream2_out->path, - "Both downstream builds should resolve to the same content-addressed output path"); - -# TODO: Once the queue runner deduplicates steps by resolved derivation -# path (not just original drv path), we should also verify that both -# original steps resolve to steps with the same derivation. (Might even -# be the same step, but that doesn't matter as much). -# -# If there are multiple steps for the single resolved derivation, -# additionally, only one should get built, and the other should be a -# cached successes (as is normal for duplicative build steps). + "Both downstream builds should create the same content-addressed output path"); + +my $downstream1_step = $db->resultset('BuildSteps')->find({ + build => $downstream1->id, + drvPath => $downstream1->drvpath, +}); + +my $downstream2_step = $db->resultset('BuildSteps')->find({ + build => $downstream2->id, + drvPath => $downstream2->drvpath, +}); + +ok(length($downstream1_step->resolveddrvpath) > 32, + "Downstream build should resolve to a valid store path"); + +is($downstream1_step->resolveddrvpath, $downstream2_step->resolveddrvpath, + "Both downstream builds should resolve to the same derivation"); + +ok($downstream1->iscachedbuild || $downstream2->iscachedbuild, + "One downstream build should be cached"); done_testing; diff --git a/subprojects/hydra-tests/content-addressed/dyn-drv.t b/subprojects/hydra-tests/content-addressed/dyn-drv.t index 6a6e907fc..cb6bf6c30 100644 --- a/subprojects/hydra-tests/content-addressed/dyn-drv.t +++ b/subprojects/hydra-tests/content-addressed/dyn-drv.t @@ -4,6 +4,10 @@ use warnings; use Setup; use Test2::V0; +# FIXME now that we're properly resolving things in Hydra rather than Nix, +# dynamic derivations stopped-fake working +plan skip_all => 'dynamic derivation resolution not yet implemented'; + # Based on https://github.com/NixOS/nix/blob/14ffc1787182b8702910788aea02bd5804afb32e/tests/functional/dyn-drv/text-hashed-output.nix # # A single derivation produces a .drv file as its output; another diff --git a/subprojects/hydra/lib/Hydra/Schema/Result/BuildSteps.pm b/subprojects/hydra/lib/Hydra/Schema/Result/BuildSteps.pm index 02e4e8c17..97caf85aa 100644 --- a/subprojects/hydra/lib/Hydra/Schema/Result/BuildSteps.pm +++ b/subprojects/hydra/lib/Hydra/Schema/Result/BuildSteps.pm @@ -113,6 +113,11 @@ __PACKAGE__->table("buildsteps"); data_type: 'boolean' is_nullable: 1 +=head2 resolveddrvpath + + data_type: 'text' + is_nullable: 1 + =cut __PACKAGE__->add_columns( @@ -146,6 +151,8 @@ __PACKAGE__->add_columns( { data_type => "integer", is_nullable => 1 }, "isnondeterministic", { data_type => "boolean", is_nullable => 1 }, + "resolveddrvpath", + { data_type => "text", is_nullable => 1 }, ); =head1 PRIMARY KEY @@ -215,8 +222,8 @@ __PACKAGE__->belongs_to( ); -# Created by DBIx::Class::Schema::Loader v0.07049 @ 2021-08-26 12:02:36 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:GzztRd7OwomaT3Xi7NB2RQ +# Created by DBIx::Class::Schema::Loader v0.07051 @ 2026-04-14 13:27:30 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Ktg5f7JBFiPfboSb0HBsMQ my %hint = ( columns => [ @@ -225,6 +232,7 @@ my %hint = ( "stepnr", "drvpath", "starttime", + "resolveddrvpath", ], eager_relations => { build => 'id' diff --git a/subprojects/hydra/sql/hydra.sql b/subprojects/hydra/sql/hydra.sql index 11b270138..9d2b20024 100644 --- a/subprojects/hydra/sql/hydra.sql +++ b/subprojects/hydra/sql/hydra.sql @@ -293,6 +293,16 @@ create table BuildSteps ( -- Whether this build step produced different results when repeated. isNonDeterministic boolean, + -- If this step was resolved to a different CA derivation, stores the + -- resolved drv path so outputs can be looked up by drv path. + -- + -- Note: Unlike the other store path fields, this one does *not* include + -- the store dir. Eventually, it would be nice to migrate the other fields + -- to also not include the store dir, as it is repeated / denormalizes the + -- database. There is nothing special about this field that makes it not + -- include it, it is just newer. + resolvedDrvPath text, + primary key (build, stepnr), foreign key (build) references Builds(id) on delete cascade, foreign key (propagatedFrom) references Builds(id) on delete cascade diff --git a/subprojects/hydra/sql/upgrade-86.sql b/subprojects/hydra/sql/upgrade-86.sql new file mode 100644 index 000000000..e39846ac2 --- /dev/null +++ b/subprojects/hydra/sql/upgrade-86.sql @@ -0,0 +1 @@ +ALTER TABLE BuildSteps ADD COLUMN resolvedDrvPath text; diff --git a/subprojects/proto/v1/streaming.proto b/subprojects/proto/v1/streaming.proto index a450f6630..c94986180 100644 --- a/subprojects/proto/v1/streaming.proto +++ b/subprojects/proto/v1/streaming.proto @@ -125,7 +125,7 @@ message PresignedUploadOpts { message BuildMessage { string build_id = 1; // UUIDv4 StorePath drv = 2; - optional StorePath resolved_drv = 3; + reserved 3; uint64 max_log_size = 4; int32 max_silent_time = 5; int32 build_timeout = 6;