1
1
Fork 0
mirror of https://github.com/NixOS/nix.git synced 2025-11-08 11:36:03 +01:00

Merge pull request #14426 from obsidiansystems/json-schema-build-result

JSON Impl and schema for BuildResult
This commit is contained in:
John Ericson 2025-11-06 18:40:35 +00:00 committed by GitHub
commit 147e183c68
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 467 additions and 0 deletions

View file

@ -44,6 +44,7 @@ mkMesonDerivation (finalAttrs: {
../../src/libstore-tests/data/derived-path
../../src/libstore-tests/data/path-info
../../src/libstore-tests/data/nar-info
../../src/libstore-tests/data/build-result
# Too many different types of files to filter for now
../../doc/manual
./.

View file

@ -127,6 +127,7 @@
- [Derivation](protocols/json/derivation.md)
- [Deriving Path](protocols/json/deriving-path.md)
- [Build Trace Entry](protocols/json/build-trace-entry.md)
- [Build Result](protocols/json/build-result.md)
- [Serving Tarball Flakes](protocols/tarball-fetcher.md)
- [Store Path Specification](protocols/store-path.md)
- [Nix Archive (NAR) Format](protocols/nix-archive/index.md)

View file

@ -0,0 +1,21 @@
{{#include build-result-v1-fixed.md}}
## Examples
### Successful build
```json
{{#include schema/build-result-v1/success.json}}
```
### Failed build (output rejected)
```json
{{#include schema/build-result-v1/output-rejected.json}}
```
### Failed build (non-deterministic)
```json
{{#include schema/build-result-v1/not-deterministic.json}}
```

View file

@ -16,6 +16,7 @@ schemas = [
'derivation-v3',
'deriving-path-v1',
'build-trace-entry-v1',
'build-result-v1',
]
schema_files = files()

View file

@ -0,0 +1 @@
../../../../../../src/libstore-tests/data/build-result

View file

@ -0,0 +1,136 @@
"$schema": "http://json-schema.org/draft-04/schema"
"$id": "https://nix.dev/manual/nix/latest/protocols/json/schema/build-result-v1.json"
title: Build Result
description: |
This schema describes the JSON representation of Nix's `BuildResult` type, which represents the result of building a derivation or substituting store paths.
Build results can represent either successful builds (with built outputs) or various types of failures.
oneOf:
- "$ref": "#/$defs/success"
- "$ref": "#/$defs/failure"
type: object
required:
- success
- status
properties:
timesBuilt:
type: integer
minimum: 0
title: Times built
description: |
How many times this build was performed.
startTime:
type: integer
minimum: 0
title: Start time
description: |
The start time of the build (or one of the rounds, if it was repeated), as a Unix timestamp.
stopTime:
type: integer
minimum: 0
title: Stop time
description: |
The stop time of the build (or one of the rounds, if it was repeated), as a Unix timestamp.
cpuUser:
type: integer
minimum: 0
title: User CPU time
description: |
User CPU time the build took, in microseconds.
cpuSystem:
type: integer
minimum: 0
title: System CPU time
description: |
System CPU time the build took, in microseconds.
"$defs":
success:
type: object
title: Successful Build Result
description: |
Represents a successful build with built outputs.
required:
- success
- status
- builtOutputs
properties:
success:
const: true
title: Success indicator
description: |
Always true for successful build results.
status:
type: string
title: Success status
description: |
Status string for successful builds.
enum:
- "Built"
- "Substituted"
- "AlreadyValid"
- "ResolvesToAlreadyValid"
builtOutputs:
type: object
title: Built outputs
description: |
A mapping from output names to their build trace entries.
additionalProperties:
"$ref": "build-trace-entry-v1.yaml"
failure:
type: object
title: Failed Build Result
description: |
Represents a failed build with error information.
required:
- success
- status
- errorMsg
properties:
success:
const: false
title: Success indicator
description: |
Always false for failed build results.
status:
type: string
title: Failure status
description: |
Status string for failed builds.
enum:
- "PermanentFailure"
- "InputRejected"
- "OutputRejected"
- "TransientFailure"
- "CachedFailure"
- "TimedOut"
- "MiscFailure"
- "DependencyFailed"
- "LogLimitExceeded"
- "NotDeterministic"
- "NoSubstituters"
- "HashMismatch"
errorMsg:
type: string
title: Error message
description: |
Information about the error if the build failed.
isNonDeterministic:
type: boolean
title: Non-deterministic flag
description: |
If timesBuilt > 1, whether some builds did not produce the same result.
Note that 'isNonDeterministic = false' does not mean the build is deterministic,
just that we don't have evidence of non-determinism.

View file

@ -0,0 +1 @@
../../src/libstore-tests/data/build-result

View file

@ -150,6 +150,15 @@ schemas += [
'impure.json',
],
},
{
'stem' : 'build-result',
'schema' : schema_dir / 'build-result-v1.yaml',
'files' : [
'success.json',
'output-rejected.json',
'not-deterministic.json',
],
},
# Match exact variant
{
'stem' : 'store-object-info',

View file

@ -28,6 +28,7 @@ mkMesonDerivation (finalAttrs: {
../../src/libstore-tests/data/derived-path
../../src/libstore-tests/data/path-info
../../src/libstore-tests/data/nar-info
../../src/libstore-tests/data/build-result
./.
];

View file

@ -0,0 +1,108 @@
#include <gtest/gtest.h>
#include "nix/store/build-result.hh"
#include "nix/util/tests/json-characterization.hh"
namespace nix {
class BuildResultTest : public virtual CharacterizationTest
{
std::filesystem::path unitTestData = getUnitTestData() / "build-result";
public:
std::filesystem::path goldenMaster(std::string_view testStem) const override
{
return unitTestData / testStem;
}
};
using nlohmann::json;
struct BuildResultJsonTest : BuildResultTest,
JsonCharacterizationTest<BuildResult>,
::testing::WithParamInterface<std::pair<std::string_view, BuildResult>>
{};
TEST_P(BuildResultJsonTest, from_json)
{
auto & [name, expected] = GetParam();
readJsonTest(name, expected);
}
TEST_P(BuildResultJsonTest, to_json)
{
auto & [name, value] = GetParam();
writeJsonTest(name, value);
}
using namespace std::literals::chrono_literals;
INSTANTIATE_TEST_SUITE_P(
BuildResultJSON,
BuildResultJsonTest,
::testing::Values(
std::pair{
"not-deterministic",
BuildResult{
.inner{BuildResult::Failure{
.status = BuildResult::Failure::NotDeterministic,
.errorMsg = "no idea why",
.isNonDeterministic = false, // Note: This field is separate from the status
}},
.timesBuilt = 1,
},
},
std::pair{
"output-rejected",
BuildResult{
.inner{BuildResult::Failure{
.status = BuildResult::Failure::OutputRejected,
.errorMsg = "no idea why",
.isNonDeterministic = false,
}},
.timesBuilt = 3,
.startTime = 30,
.stopTime = 50,
},
},
std::pair{
"success",
BuildResult{
.inner{BuildResult::Success{
.status = BuildResult::Success::Built,
.builtOutputs{
{
"foo",
{
{
.outPath = StorePath{"g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-foo"},
},
DrvOutput{
.drvHash = Hash::parseSRI("sha256-b4afnqKCO9oWXgYHb9DeQ2berSwOjS27rSd9TxXDc/U="),
.outputName = "foo",
},
},
},
{
"bar",
{
{
.outPath = StorePath{"g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar"},
},
DrvOutput{
.drvHash = Hash::parseSRI("sha256-b4afnqKCO9oWXgYHb9DeQ2berSwOjS27rSd9TxXDc/U="),
.outputName = "bar",
},
},
},
},
}},
.timesBuilt = 3,
.startTime = 30,
.stopTime = 50,
.cpuUser = std::chrono::microseconds(500s),
.cpuSystem = std::chrono::microseconds(604s),
},
}));
} // namespace nix

View file

@ -0,0 +1,9 @@
{
"errorMsg": "no idea why",
"isNonDeterministic": false,
"startTime": 0,
"status": "NotDeterministic",
"stopTime": 0,
"success": false,
"timesBuilt": 1
}

View file

@ -0,0 +1,9 @@
{
"errorMsg": "no idea why",
"isNonDeterministic": false,
"startTime": 30,
"status": "OutputRejected",
"stopTime": 50,
"success": false,
"timesBuilt": 3
}

View file

@ -0,0 +1,23 @@
{
"builtOutputs": {
"bar": {
"dependentRealisations": {},
"id": "sha256:6f869f9ea2823bda165e06076fd0de4366dead2c0e8d2dbbad277d4f15c373f5!bar",
"outPath": "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar",
"signatures": []
},
"foo": {
"dependentRealisations": {},
"id": "sha256:6f869f9ea2823bda165e06076fd0de4366dead2c0e8d2dbbad277d4f15c373f5!foo",
"outPath": "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-foo",
"signatures": []
}
},
"cpuSystem": 604000000,
"cpuUser": 500000000,
"startTime": 30,
"status": "Built",
"stopTime": 50,
"success": true,
"timesBuilt": 3
}

View file

@ -54,6 +54,7 @@ deps_private += gtest
subdir('nix-meson-build-support/common')
sources = files(
'build-result.cc',
'common-protocol.cc',
'content-address.cc',
'derivation-advanced-attrs.cc',

View file

@ -1,4 +1,6 @@
#include "nix/store/build-result.hh"
#include "nix/util/json-utils.hh"
#include <array>
namespace nix {
@ -11,4 +13,144 @@ std::strong_ordering BuildResult::Success::operator<=>(const BuildResult::Succes
bool BuildResult::Failure::operator==(const BuildResult::Failure &) const noexcept = default;
std::strong_ordering BuildResult::Failure::operator<=>(const BuildResult::Failure &) const noexcept = default;
static constexpr std::array<std::pair<BuildResult::Success::Status, std::string_view>, 4> successStatusStrings{{
#define ENUM_ENTRY(e) {BuildResult::Success::e, #e}
ENUM_ENTRY(Built),
ENUM_ENTRY(Substituted),
ENUM_ENTRY(AlreadyValid),
ENUM_ENTRY(ResolvesToAlreadyValid),
#undef ENUM_ENTRY
}};
static std::string_view successStatusToString(BuildResult::Success::Status status)
{
for (const auto & [enumVal, str] : successStatusStrings) {
if (enumVal == status)
return str;
}
throw Error("unknown success status: %d", static_cast<int>(status));
}
static BuildResult::Success::Status successStatusFromString(std::string_view str)
{
for (const auto & [enumVal, enumStr] : successStatusStrings) {
if (enumStr == str)
return enumVal;
}
throw Error("unknown built result success status '%s'", str);
}
static constexpr std::array<std::pair<BuildResult::Failure::Status, std::string_view>, 12> failureStatusStrings{{
#define ENUM_ENTRY(e) {BuildResult::Failure::e, #e}
ENUM_ENTRY(PermanentFailure),
ENUM_ENTRY(InputRejected),
ENUM_ENTRY(OutputRejected),
ENUM_ENTRY(TransientFailure),
ENUM_ENTRY(CachedFailure),
ENUM_ENTRY(TimedOut),
ENUM_ENTRY(MiscFailure),
ENUM_ENTRY(DependencyFailed),
ENUM_ENTRY(LogLimitExceeded),
ENUM_ENTRY(NotDeterministic),
ENUM_ENTRY(NoSubstituters),
ENUM_ENTRY(HashMismatch),
#undef ENUM_ENTRY
}};
static std::string_view failureStatusToString(BuildResult::Failure::Status status)
{
for (const auto & [enumVal, str] : failureStatusStrings) {
if (enumVal == status)
return str;
}
throw Error("unknown failure status: %d", static_cast<int>(status));
}
static BuildResult::Failure::Status failureStatusFromString(std::string_view str)
{
for (const auto & [enumVal, enumStr] : failureStatusStrings) {
if (enumStr == str)
return enumVal;
}
throw Error("unknown built result failure status '%s'", str);
}
} // namespace nix
namespace nlohmann {
using namespace nix;
void adl_serializer<BuildResult>::to_json(json & res, const BuildResult & br)
{
res = json::object();
// Common fields
res["timesBuilt"] = br.timesBuilt;
res["startTime"] = br.startTime;
res["stopTime"] = br.stopTime;
if (br.cpuUser.has_value()) {
res["cpuUser"] = br.cpuUser->count();
}
if (br.cpuSystem.has_value()) {
res["cpuSystem"] = br.cpuSystem->count();
}
// Handle success or failure variant
std::visit(
overloaded{
[&](const BuildResult::Success & success) {
res["success"] = true;
res["status"] = successStatusToString(success.status);
res["builtOutputs"] = success.builtOutputs;
},
[&](const BuildResult::Failure & failure) {
res["success"] = false;
res["status"] = failureStatusToString(failure.status);
res["errorMsg"] = failure.errorMsg;
res["isNonDeterministic"] = failure.isNonDeterministic;
},
},
br.inner);
}
BuildResult adl_serializer<BuildResult>::from_json(const json & _json)
{
auto & json = getObject(_json);
BuildResult br;
// Common fields
br.timesBuilt = getUnsigned(valueAt(json, "timesBuilt"));
br.startTime = getUnsigned(valueAt(json, "startTime"));
br.stopTime = getUnsigned(valueAt(json, "stopTime"));
if (auto cpuUser = optionalValueAt(json, "cpuUser")) {
br.cpuUser = std::chrono::microseconds(getUnsigned(*cpuUser));
}
if (auto cpuSystem = optionalValueAt(json, "cpuSystem")) {
br.cpuSystem = std::chrono::microseconds(getUnsigned(*cpuSystem));
}
// Determine success or failure based on success field
bool success = getBoolean(valueAt(json, "success"));
std::string statusStr = getString(valueAt(json, "status"));
if (success) {
BuildResult::Success s;
s.status = successStatusFromString(statusStr);
s.builtOutputs = valueAt(json, "builtOutputs");
br.inner = std::move(s);
} else {
BuildResult::Failure f;
f.status = failureStatusFromString(statusStr);
f.errorMsg = getString(valueAt(json, "errorMsg"));
f.isNonDeterministic = getBoolean(valueAt(json, "isNonDeterministic"));
br.inner = std::move(f);
}
return br;
}
} // namespace nlohmann

View file

@ -7,6 +7,7 @@
#include "nix/store/derived-path.hh"
#include "nix/store/realisation.hh"
#include "nix/util/json-impls.hh"
namespace nix {
@ -175,3 +176,5 @@ struct KeyedBuildResult : BuildResult
};
} // namespace nix
JSON_IMPL(nix::BuildResult)