diff --git a/src/libexpr-c/meson.build b/src/libexpr-c/meson.build index 5c6dc6d666ad..fb3a17ff2998 100644 --- a/src/libexpr-c/meson.build +++ b/src/libexpr-c/meson.build @@ -30,6 +30,7 @@ subdir('nix-meson-build-support/subprojects') subdir('nix-meson-build-support/common') sources = files( + 'nix_api_eval.cc', 'nix_api_expr.cc', 'nix_api_external.cc', 'nix_api_value.cc', diff --git a/src/libexpr-c/nix_api_eval.cc b/src/libexpr-c/nix_api_eval.cc new file mode 100644 index 000000000000..4c7e2e08fc35 --- /dev/null +++ b/src/libexpr-c/nix_api_eval.cc @@ -0,0 +1,55 @@ +#include "nix/expr/eval.hh" +#include "nix/expr/get-drvs.hh" + +#include "nix_api_expr.h" +#include "nix_api_expr_internal.h" +#include "nix_api_store.h" +#include "nix_api_store_internal.h" +#include "nix_api_util.h" +#include "nix_api_util_internal.h" + +static const nix::Bindings & get_bindings_or_empty(nix::EvalState & state, nix_value * autoArgs) +{ + if (!autoArgs) { + return nix::Bindings::emptyBindings; + } + auto & v = check_value_in(autoArgs); + state.forceAttrs(v, nix::noPos, "while evaluating automatic function arguments"); + return *v.attrs(); +} + +extern "C" { + +StorePath * +nix_get_derivation(nix_c_context * context, EvalState * state, nix_value * value, bool ignoreAssertionFailures) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto & v = check_value_in(value); + auto maybePkg = nix::getDerivation(state->state, v, ignoreAssertionFailures); + if (!maybePkg) { + return nullptr; + } + nix::StorePath sp = maybePkg->requireDrvPath(); + return new StorePath{std::move(sp)}; + } + NIXC_CATCH_ERRS_NULL +} + +nix_err nix_value_auto_call_function( + nix_c_context * context, EvalState * state, nix_value * auto_args, nix_value * fn_val, nix_value * result) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto & fn = check_value_in(fn_val); + auto & res = check_value_not_null(result); + + auto & b = get_bindings_or_empty(state->state, auto_args); + state->state.autoCallFunction(b, fn, res); + } + NIXC_CATCH_ERRS +} + +} // extern "C" diff --git a/src/libexpr-c/nix_api_expr.h b/src/libexpr-c/nix_api_expr.h index 3623ee076f6f..0326e7becf4a 100644 --- a/src/libexpr-c/nix_api_expr.h +++ b/src/libexpr-c/nix_api_expr.h @@ -342,6 +342,69 @@ void nix_gc_register_finalizer(void * obj, void * cd, void (*finalizer)(void * o /** @} */ // doxygen group GC +/** @defgroup libexpr_eval Evaluation + * @ingroup libexpr + * @brief Higher-level evaluation helpers + * @{ + */ + +/** + * @brief Determine whether a Nix value is a derivation and, if so, return its + * store derivation path. + * + * Forces @p value and inspects it. The value is considered a derivation when it + * is an attribute set whose `type` attribute is the string `"derivation"`; in + * that case its `drvPath` attribute is parsed and returned. Otherwise NULL is + * returned without recording an error. + * + * Only the derivation path is returned. Other metadata (`name`, `system`, + * outputs, `meta`, ...) lives on @p value itself and can be read with the + * attribute-set accessors such as nix_get_attr_byname() and nix_get_string(). + * + * @param[out] context Optional, stores error information. On a NULL return, + * inspect the error code via nix_err_code() to tell the two NULL cases apart: + * NIX_OK means @p value is simply not a derivation, any other code means + * inspection failed. See @ref errors. + * @param[in] state The evaluation state. + * @param[in] value The value to inspect. It is forced by this call. + * @param[in] ignoreAssertionFailures If true, an assertion failure raised while + * forcing @p value is treated as "not a derivation" (NULL is returned without + * an error) rather than being reported as an error. + * @return A newly allocated StorePath holding the derivation path, or NULL. + * Free a non-NULL result with nix_store_path_free(). + */ +StorePath * +nix_get_derivation(nix_c_context * context, EvalState * state, nix_value * value, bool ignoreAssertionFailures); + +/** + * @brief Call a function, drawing its arguments from an attribute set. + * + * Forces @p fn_val and writes the application result into @p result. The result + * is not forced; call nix_value_force() to evaluate it before inspecting the + * final value. + * + * - If @p fn_val is a function that takes a set of named arguments + * (e.g. `{ a, b ? 1 }: ...`), it is called with an attribute set assembled + * from @p auto_args: each named argument is taken from @p auto_args when + * present; an argument absent from @p auto_args falls back to its default; + * an argument that is both absent and has no default is an error. + * - Otherwise @p fn_val is copied into @p result unchanged. This includes any + * non-function value as well as a function that takes a single unnamed + * argument (e.g. `x: ...`), since there are no named arguments to supply. + * + * @param[out] context Optional, stores error information + * @param[in] state The evaluation state. + * @param[in] auto_args Attribute set value supplying the named arguments, or + * NULL to supply none. + * @param[in] fn_val The value to call. + * @param[out] result Pre-allocated nix_value that receives the result. + * @return NIX_OK if the call was successful, an error code otherwise. + */ +nix_err nix_value_auto_call_function( + nix_c_context * context, EvalState * state, nix_value * auto_args, nix_value * fn_val, nix_value * result); + +/** @} */ // doxygen group libexpr_eval + // cffi end #ifdef __cplusplus } diff --git a/src/libexpr-c/nix_api_expr_internal.h b/src/libexpr-c/nix_api_expr_internal.h index 3f7e4bf1df12..16b32bfa4602 100644 --- a/src/libexpr-c/nix_api_expr_internal.h +++ b/src/libexpr-c/nix_api_expr_internal.h @@ -2,6 +2,7 @@ #define NIX_API_EXPR_INTERNAL_H #include +#include #include "nix/fetchers/fetch-settings.hh" #include "nix/expr/eval.hh" @@ -74,4 +75,35 @@ struct nix_realised_string } // extern "C" +// Shared helpers for validating nix_value [in] parameters across libexpr-c translation units. +inline const nix::Value & check_value_not_null(const nix_value * value) +{ + if (!value || !value->value) + throw std::runtime_error("nix_value is null"); + return *value->value; +} + +inline nix::Value & check_value_not_null(nix_value * value) +{ + if (!value || !value->value) + throw std::runtime_error("nix_value is null"); + return *value->value; +} + +inline const nix::Value & check_value_in(const nix_value * value) +{ + auto & v = check_value_not_null(value); + if (!v.isValid()) + throw std::runtime_error("Uninitialized nix_value"); + return v; +} + +inline nix::Value & check_value_in(nix_value * value) +{ + auto & v = check_value_not_null(value); + if (!v.isValid()) + throw std::runtime_error("Uninitialized nix_value"); + return v; +} + #endif // NIX_API_EXPR_INTERNAL_H diff --git a/src/libexpr-c/nix_api_value.cc b/src/libexpr-c/nix_api_value.cc index f15a4aa836a9..eaa5c2e1d535 100644 --- a/src/libexpr-c/nix_api_value.cc +++ b/src/libexpr-c/nix_api_value.cc @@ -12,41 +12,6 @@ #include "nix_api_store_internal.h" #include "nix_api_value.h" -// Internal helper functions to check [in] and [out] `Value *` parameters -static const nix::Value & check_value_not_null(const nix_value * value) -{ - if (!value) { - throw std::runtime_error("nix_value is null"); - } - return *value->value; -} - -static nix::Value & check_value_not_null(nix_value * value) -{ - if (!value) { - throw std::runtime_error("nix_value is null"); - } - return *value->value; -} - -static const nix::Value & check_value_in(const nix_value * value) -{ - auto & v = check_value_not_null(value); - if (!v.isValid()) { - throw std::runtime_error("Uninitialized nix_value"); - } - return v; -} - -static nix::Value & check_value_in(nix_value * value) -{ - auto & v = check_value_not_null(value); - if (!v.isValid()) { - throw std::runtime_error("Uninitialized nix_value"); - } - return v; -} - static nix::Value & check_value_out(nix_value * value) { auto & v = check_value_not_null(value); diff --git a/src/libexpr-tests/meson.build b/src/libexpr-tests/meson.build index 50d158209ba7..0927ee24b2b2 100644 --- a/src/libexpr-tests/meson.build +++ b/src/libexpr-tests/meson.build @@ -53,6 +53,7 @@ sources = files( 'json.cc', 'lazy-fetcher-attr.cc', 'main.cc', + 'nix_api_eval.cc', 'nix_api_expr.cc', 'nix_api_external.cc', 'nix_api_value.cc', diff --git a/src/libexpr-tests/nix_api_eval.cc b/src/libexpr-tests/nix_api_eval.cc new file mode 100644 index 000000000000..9d2117678735 --- /dev/null +++ b/src/libexpr-tests/nix_api_eval.cc @@ -0,0 +1,259 @@ +#include "nix_api_store.h" +#include "nix_api_util.h" +#include "nix_api_expr.h" +#include "nix_api_value.h" + +#include "nix/expr/tests/nix_api_expr.hh" +#include "nix/util/tests/string_callback.hh" +#include "nix/util/tests/gmock-matchers.hh" + +#include +#include + +namespace nixC { + +// nix_get_derivation + +TEST_F(nix_api_expr_test, nix_get_derivation_returns_drv_path) +{ + auto expr = R"(derivation { name = "myname"; builder = "mybuilder"; system = "mysystem"; })"; + nix_expr_eval_from_string(ctx, state, expr, ".", value); + assert_ctx_ok(); + + StorePath * drvPath = nix_get_derivation(ctx, state, value, false); + assert_ctx_ok(); + ASSERT_NE(nullptr, drvPath); + + std::string name; + nix_store_path_name(drvPath, OBSERVE_STRING(name)); + EXPECT_THAT(name, ::testing::HasSubstr("myname")); + EXPECT_THAT(name, ::testing::EndsWith(".drv")); + + nix_store_path_free(drvPath); +} + +TEST_F(nix_api_expr_test, nix_get_derivation_non_derivation_returns_null_without_error) +{ + nix_expr_eval_from_string(ctx, state, "{ a = 1; }", ".", value); + assert_ctx_ok(); + + StorePath * drvPath = nix_get_derivation(ctx, state, value, false); + // Not a derivation: NULL with no error recorded, so the caller can tell + // this apart from a genuine failure. + ASSERT_EQ(nullptr, drvPath); + ASSERT_EQ(NIX_OK, nix_err_code(ctx)); +} + +TEST_F(nix_api_expr_test, nix_get_derivation_non_attrset_returns_null_without_error) +{ + nix_expr_eval_from_string(ctx, state, "42", ".", value); + assert_ctx_ok(); + + StorePath * drvPath = nix_get_derivation(ctx, state, value, false); + ASSERT_EQ(nullptr, drvPath); + ASSERT_EQ(NIX_OK, nix_err_code(ctx)); +} + +TEST_F(nix_api_expr_test, nix_get_derivation_null_value_is_error) +{ + StorePath * drvPath = nix_get_derivation(ctx, state, nullptr, false); + ASSERT_EQ(nullptr, drvPath); + ASSERT_NE(NIX_OK, nix_err_code(ctx)); +} + +// A derivation-shaped attribute set whose `name` throws an assertion only when +// forced. The outer set is already WHNF, so nix_get_derivation is what triggers +// the failure (while reading the name), exercising the assertion handling. +static constexpr const char * ASSERTING_DRV = R"({ type = "derivation"; name = assert false; "myname"; })"; + +TEST_F(nix_api_expr_test, nix_get_derivation_assertion_ignored) +{ + nix_expr_eval_from_string(ctx, state, ASSERTING_DRV, ".", value); + assert_ctx_ok(); + + // With ignoreAssertionFailures = true the assertion is swallowed and the + // value is reported as "not a derivation": NULL with no error. + StorePath * drvPath = nix_get_derivation(ctx, state, value, true); + ASSERT_EQ(nullptr, drvPath); + ASSERT_EQ(NIX_OK, nix_err_code(ctx)); +} + +TEST_F(nix_api_expr_test, nix_get_derivation_assertion_propagated) +{ + nix_expr_eval_from_string(ctx, state, ASSERTING_DRV, ".", value); + assert_ctx_ok(); + + // With ignoreAssertionFailures = false the assertion surfaces as an error. + StorePath * drvPath = nix_get_derivation(ctx, state, value, false); + ASSERT_EQ(nullptr, drvPath); + ASSERT_EQ(NIX_ERR_NIX_ERROR, nix_err_code(ctx)); + ASSERT_THAT(nix_err_msg(nullptr, ctx, nullptr), ::testing::HasSubstr("assert")); +} + +// nix_value_auto_call_function + +TEST_F(nix_api_expr_test, nix_value_auto_call_function_supplies_args) +{ + nix_expr_eval_from_string(ctx, state, "{ a, b }: a + b", ".", value); + assert_ctx_ok(); + + nix_value * args = nix_alloc_value(ctx, state); + nix_expr_eval_from_string(ctx, state, "{ a = 1; b = 2; }", ".", args); + assert_ctx_ok(); + + nix_value * result = nix_alloc_value(ctx, state); + nix_value_auto_call_function(ctx, state, args, value, result); + assert_ctx_ok(); + + ASSERT_EQ(3, nix_get_int(ctx, result)); + + nix_gc_decref(ctx, args); + nix_gc_decref(ctx, result); +} + +TEST_F(nix_api_expr_test, nix_value_auto_call_function_uses_defaults) +{ + nix_expr_eval_from_string(ctx, state, "{ a, b ? 10 }: a + b", ".", value); + assert_ctx_ok(); + + nix_value * args = nix_alloc_value(ctx, state); + nix_expr_eval_from_string(ctx, state, "{ a = 5; }", ".", args); + assert_ctx_ok(); + + nix_value * result = nix_alloc_value(ctx, state); + nix_value_auto_call_function(ctx, state, args, value, result); + assert_ctx_ok(); + + ASSERT_EQ(15, nix_get_int(ctx, result)); + + nix_gc_decref(ctx, args); + nix_gc_decref(ctx, result); +} + +TEST_F(nix_api_expr_test, nix_value_auto_call_function_forces_auto_args) +{ + nix_expr_eval_from_string(ctx, state, "{ a }: a + 1", ".", value); + assert_ctx_ok(); + + nix_value * identity = nix_alloc_value(ctx, state); + nix_expr_eval_from_string(ctx, state, "x: x", ".", identity); + assert_ctx_ok(); + + nix_value * attrs = nix_alloc_value(ctx, state); + nix_expr_eval_from_string(ctx, state, "{ a = 4; }", ".", attrs); + assert_ctx_ok(); + + nix_value * args = nix_alloc_value(ctx, state); + nix_init_apply(ctx, args, identity, attrs); + assert_ctx_ok(); + + nix_value * result = nix_alloc_value(ctx, state); + nix_value_auto_call_function(ctx, state, args, value, result); + assert_ctx_ok(); + + ASSERT_EQ(5, nix_get_int(ctx, result)); + + nix_gc_decref(ctx, identity); + nix_gc_decref(ctx, attrs); + nix_gc_decref(ctx, args); + nix_gc_decref(ctx, result); +} + +TEST_F(nix_api_expr_test, nix_value_auto_call_function_non_attr_auto_args_is_error) +{ + nix_expr_eval_from_string(ctx, state, "{ a ? 7 }: a", ".", value); + assert_ctx_ok(); + + nix_value * args = nix_alloc_value(ctx, state); + nix_init_int(ctx, args, 42); + assert_ctx_ok(); + + nix_value * result = nix_alloc_value(ctx, state); + nix_value_auto_call_function(ctx, state, args, value, result); + ASSERT_EQ(NIX_ERR_NIX_ERROR, nix_err_code(ctx)); + ASSERT_THAT(nix_err_msg(nullptr, ctx, nullptr), ::nix::testing::HasSubstrIgnoreANSIMatcher("expected a set")); + + nix_gc_decref(ctx, args); + nix_gc_decref(ctx, result); +} + +TEST_F(nix_api_expr_test, nix_value_auto_call_function_null_args_uses_defaults) +{ + nix_expr_eval_from_string(ctx, state, "{ a ? 7 }: a", ".", value); + assert_ctx_ok(); + + nix_value * result = nix_alloc_value(ctx, state); + // NULL auto_args supplies no arguments; every formal must then have a + // default. + nix_value_auto_call_function(ctx, state, nullptr, value, result); + assert_ctx_ok(); + + ASSERT_EQ(7, nix_get_int(ctx, result)); + + nix_gc_decref(ctx, result); +} + +TEST_F(nix_api_expr_test, nix_value_auto_call_function_missing_arg_is_error) +{ + nix_expr_eval_from_string(ctx, state, "{ a, b }: a + b", ".", value); + assert_ctx_ok(); + + nix_value * args = nix_alloc_value(ctx, state); + nix_expr_eval_from_string(ctx, state, "{ a = 1; }", ".", args); + assert_ctx_ok(); + + nix_value * result = nix_alloc_value(ctx, state); + // 'b' has neither a supplied value nor a default. The argument name in the + // message is colorized, so use the ANSI-stripping matcher to assert on it. + nix_value_auto_call_function(ctx, state, args, value, result); + ASSERT_EQ(NIX_ERR_NIX_ERROR, nix_err_code(ctx)); + ASSERT_THAT( + nix_err_msg(nullptr, ctx, nullptr), + ::nix::testing::HasSubstrIgnoreANSIMatcher( + "cannot evaluate a function that has an argument without a value ('b')")); + + nix_gc_decref(ctx, args); + nix_gc_decref(ctx, result); +} + +TEST_F(nix_api_expr_test, nix_value_auto_call_function_non_function_passthrough) +{ + nix_expr_eval_from_string(ctx, state, "42", ".", value); + assert_ctx_ok(); + + nix_value * result = nix_alloc_value(ctx, state); + // A non-function value is returned unchanged. + nix_value_auto_call_function(ctx, state, nullptr, value, result); + assert_ctx_ok(); + + ASSERT_EQ(42, nix_get_int(ctx, result)); + + nix_gc_decref(ctx, result); +} + +TEST_F(nix_api_expr_test, nix_value_auto_call_function_single_arg_lambda_passthrough) +{ + nix_expr_eval_from_string(ctx, state, "x: x + 1", ".", value); + assert_ctx_ok(); + + nix_value * result = nix_alloc_value(ctx, state); + // A function taking a single unnamed argument has no named arguments to + // fill, so it is returned unchanged rather than being called. + nix_value_auto_call_function(ctx, state, nullptr, value, result); + assert_ctx_ok(); + + ASSERT_EQ(NIX_TYPE_FUNCTION, nix_get_type(ctx, result)); + + nix_gc_decref(ctx, result); +} + +TEST_F(nix_api_expr_test, nix_value_auto_call_function_null_fn_is_error) +{ + nix_value * result = nix_alloc_value(ctx, state); + nix_value_auto_call_function(ctx, state, nullptr, nullptr, result); + ASSERT_NE(NIX_OK, nix_err_code(ctx)); + + nix_gc_decref(ctx, result); +} + +} // namespace nixC