diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 526fecabf..59db217d9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -14,4 +14,4 @@ src/libexpr/primops.cc @roberth # Libstore layer -/src/libstore @thufschmitt +/src/libstore @thufschmitt @ericson2314 diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 5b75704b5..8f83b913c 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -21,7 +21,7 @@ jobs: fetch-depth: 0 - name: Create backport PRs # should be kept in sync with `version` - uses: zeebe-io/backport-action@v2.4.1 + uses: zeebe-io/backport-action@v2.5.0 with: # Config README: https://github.com/zeebe-io/backport-action#backport-action github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8bd355cca..2b8eac49d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -159,3 +159,11 @@ jobs: # deprecated 2024-02-24 docker tag nix:$NIX_VERSION $IMAGE_ID:master docker push $IMAGE_ID:master + + vm_tests: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - uses: DeterminateSystems/nix-installer-action@main + - uses: DeterminateSystems/magic-nix-cache-action@main + - run: nix build -L .#hydraJobs.tests.githubFlakes .#hydraJobs.tests.tarballFlakes diff --git a/.gitignore b/.gitignore index 01fafa5a9..5a33c00ea 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,9 @@ perl/Makefile.config /src/libexpr/tests /tests/unit/libexpr/libnixexpr-tests +# /src/libfetchers +/tests/unit/libfetchers/libnixfetchers-tests + # /src/libstore/ *.gen.* /src/libstore/tests diff --git a/Makefile b/Makefile index c3dc83c77..62194278d 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,9 @@ makefiles = \ src/libexpr/local.mk \ src/libcmd/local.mk \ src/nix/local.mk \ + src/libutil-c/local.mk \ + src/libstore-c/local.mk \ + src/libexpr-c/local.mk \ src/resolve-system-dependencies/local.mk \ scripts/local.mk \ misc/bash/local.mk \ @@ -34,6 +37,7 @@ makefiles += \ tests/unit/libutil-support/local.mk \ tests/unit/libstore/local.mk \ tests/unit/libstore-support/local.mk \ + tests/unit/libfetchers/local.mk \ tests/unit/libexpr/local.mk \ tests/unit/libexpr-support/local.mk endif @@ -59,6 +63,10 @@ ifeq ($(ENABLE_INTERNAL_API_DOCS), yes) makefiles-late += doc/internal-api/local.mk endif +ifeq ($(ENABLE_EXTERNAL_API_DOCS), yes) +makefiles-late += doc/external-api/local.mk +endif + # Miscellaneous global Flags OPTIMIZE = 1 @@ -123,3 +131,10 @@ internal-api-html: @echo "Internal API docs are disabled. Configure with '--enable-internal-api-docs', or avoid calling 'make internal-api-html'." @exit 1 endif + +ifneq ($(ENABLE_EXTERNAL_API_DOCS), yes) +.PHONY: external-api-html +external-api-html: + @echo "External API docs are disabled. Configure with '--enable-external-api-docs', or avoid calling 'make external-api-html'." + @exit 1 +endif diff --git a/Makefile.config.in b/Makefile.config.in index d5c382630..7f517898c 100644 --- a/Makefile.config.in +++ b/Makefile.config.in @@ -12,6 +12,7 @@ ENABLE_BUILD = @ENABLE_BUILD@ ENABLE_DOC_GEN = @ENABLE_DOC_GEN@ ENABLE_FUNCTIONAL_TESTS = @ENABLE_FUNCTIONAL_TESTS@ ENABLE_INTERNAL_API_DOCS = @ENABLE_INTERNAL_API_DOCS@ +ENABLE_EXTERNAL_API_DOCS = @ENABLE_EXTERNAL_API_DOCS@ ENABLE_S3 = @ENABLE_S3@ ENABLE_UNIT_TESTS = @ENABLE_UNIT_TESTS@ GTEST_LIBS = @GTEST_LIBS@ diff --git a/configure.ac b/configure.ac index 676b145a5..1d327d51d 100644 --- a/configure.ac +++ b/configure.ac @@ -150,6 +150,11 @@ AC_ARG_ENABLE(unit-tests, AS_HELP_STRING([--disable-unit-tests],[Do not build th ENABLE_UNIT_TESTS=$enableval, ENABLE_UNIT_TESTS=$ENABLE_BUILD) AC_SUBST(ENABLE_UNIT_TESTS) +# Build external API docs by default +AC_ARG_ENABLE(external_api_docs, AS_HELP_STRING([--enable-external-api-docs],[Build API docs for Nix's C interface]), + external_api_docs=$enableval, external_api_docs=yes) +AC_SUBST(external_api_docs) + AS_IF( [test "$ENABLE_BUILD" == "no" && test "$ENABLE_UNIT_TESTS" == "yes"], [AC_MSG_ERROR([Cannot enable unit tests when building overall is disabled. Please do not pass '--enable-unit-tests' or do not pass '--disable-build'.])]) @@ -172,6 +177,10 @@ AC_ARG_ENABLE(internal-api-docs, AS_HELP_STRING([--enable-internal-api-docs],[Bu ENABLE_INTERNAL_API_DOCS=$enableval, ENABLE_INTERNAL_API_DOCS=no) AC_SUBST(ENABLE_INTERNAL_API_DOCS) +AC_ARG_ENABLE(external-api-docs, AS_HELP_STRING([--enable-external-api-docs],[Build API docs for Nix's external unstable C interfaces]), + ENABLE_EXTERNAL_API_DOCS=$enableval, ENABLE_EXTERNAL_API_DOCS=no) +AC_SUBST(ENABLE_EXTERNAL_API_DOCS) + AS_IF( [test "$ENABLE_FUNCTIONAL_TESTS" == "yes" || test "$ENABLE_DOC_GEN" == "yes"], [NEED_PROG(jq, jq)]) diff --git a/doc/external-api/.gitignore b/doc/external-api/.gitignore new file mode 100644 index 000000000..dab28b6b0 --- /dev/null +++ b/doc/external-api/.gitignore @@ -0,0 +1,3 @@ +/doxygen.cfg +/html +/latex diff --git a/doc/external-api/README.md b/doc/external-api/README.md new file mode 100644 index 000000000..8a6f1c085 --- /dev/null +++ b/doc/external-api/README.md @@ -0,0 +1,108 @@ +# Getting started + +> **Warning** These bindings are **experimental**, which means they can change +> at any time or be removed outright; nevertheless the plan is to provide a +> stable external C API to the Nix language and the Nix store. + +The language library allows evaluating Nix expressions and interacting with Nix +language values. The Nix store API is still rudimentary, and only allows +initialising and connecting to a store for the Nix language evaluator to +interact with. + +Currently there are two ways to interface with the Nix language evaluator +programmatically: + +1. Embedding the evaluator +2. Writing language plug-ins + +Embedding means you link the Nix C libraries in your program and use them from +there. Adding a plug-in means you make a library that gets loaded by the Nix +language evaluator, specified through a configuration option. + +Many of the components and mechanisms involved are not yet documented, therefore +please refer to the [Nix source code](https://github.com/NixOS/nix/) for +details. Additions to in-code documentation and the reference manual are highly +appreciated. + +The following examples, for simplicity, don't include error handling. See the +[Handling errors](@ref errors) section for more information. + +# Embedding the Nix Evaluator + +In this example we programmatically start the Nix language evaluator with a +dummy store (that has no store paths and cannot be written to), and evaluate the +Nix expression `builtins.nixVersion`. + +**main.c:** + +```C +#include +#include +#include +#include + +// NOTE: This example lacks all error handling. Production code must check for +// errors, as some return values will be undefined. +int main() { + nix_libexpr_init(NULL); + + Store* store = nix_store_open(NULL, "dummy://", NULL); + EvalState* state = nix_state_create(NULL, NULL, store); // empty search path (NIX_PATH) + Value *value = nix_alloc_value(NULL, state); + + nix_expr_eval_from_string(NULL, state, "builtins.nixVersion", ".", value); + nix_value_force(NULL, state, value); + printf("Nix version: %s\n", nix_get_string(NULL, value)); + + nix_gc_decref(NULL, value); + nix_state_free(state); + nix_store_free(store); + return 0; +} +``` + +**Usage:** + +```ShellSession +$ gcc main.c $(pkg-config nix-expr-c --libs --cflags) -o main +$ ./main +Nix version: 2.17 +``` + +# Writing a Nix language plug-in + +In this example we add a custom primitive operation (_primop_) to `builtins`. It +will increment the argument if it is an integer and throw an error otherwise. + +**plugin.c:** + +```C +#include +#include +#include + +void increment(void* user_data, nix_c_context* ctx, EvalState* state, Value** args, Value* v) { + nix_value_force(NULL, state, args[0]); + if (nix_get_type(NULL, args[0]) == NIX_TYPE_INT) { + nix_init_int(NULL, v, nix_get_int(NULL, args[0]) + 1); + } else { + nix_set_err_msg(ctx, NIX_ERR_UNKNOWN, "First argument should be an integer."); + } +} + +void nix_plugin_entry() { + const char* args[] = {"n", NULL}; + PrimOp *p = nix_alloc_primop(NULL, increment, 1, "increment", args, "Example custom built-in function: increments an integer", NULL); + nix_register_primop(NULL, p); + nix_gc_decref(NULL, p); +} +``` + +**Usage:** + +```ShellSession +$ gcc plugin.c $(pkg-config nix-expr-c --libs --cflags) -shared -o plugin.so +$ nix --plugin-files ./plugin.so repl +nix-repl> builtins.increment 1 +2 +``` diff --git a/doc/external-api/doxygen.cfg.in b/doc/external-api/doxygen.cfg.in new file mode 100644 index 000000000..cd8b4989b --- /dev/null +++ b/doc/external-api/doxygen.cfg.in @@ -0,0 +1,57 @@ +# Doxyfile 1.9.5 + +# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by +# double-quotes, unless you are using Doxywizard) that should identify the +# project for which the documentation is generated. This name is used in the +# title of most generated pages and in a few other places. +# The default value is: My Project. + +PROJECT_NAME = "Nix" + +# The PROJECT_NUMBER tag can be used to enter a project or revision number. This +# could be handy for archiving the generated documentation or if some version +# control system is used. + +PROJECT_NUMBER = @PACKAGE_VERSION@ + +# Using the PROJECT_BRIEF tag one can provide an optional one line description +# for a project that appears at the top of each page and should give viewer a +# quick idea about the purpose of the project. Keep the description short. + +PROJECT_BRIEF = "Nix, the purely functional package manager: C API (experimental)" + +# If the GENERATE_LATEX tag is set to YES, doxygen will generate LaTeX output. +# The default value is: YES. + +GENERATE_LATEX = NO + +# The INPUT tag is used to specify the files and/or directories that contain +# documented source files. You may enter file names like myfile.cpp or +# directories like /usr/src/myproject. Separate the files or directories with +# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING +# Note: If this tag is empty the current directory is searched. + +# FIXME Make this list more maintainable somehow. We could maybe generate this +# in the Makefile, but we would need to change how `.in` files are preprocessed +# so they can expand variables despite configure variables. + +INPUT = \ + src/libutil-c \ + src/libexpr-c \ + src/libstore-c \ + doc/external-api/README.md + +FILE_PATTERNS = nix_api_*.h *.md + +# The INCLUDE_PATH tag can be used to specify one or more directories that +# contain include files that are not input files but should be processed by the +# preprocessor. Note that the INCLUDE_PATH is not recursive, so the setting of +# RECURSIVE has no effect here. +# This tag requires that the tag SEARCH_INCLUDES is set to YES. + +INCLUDE_PATH = @RAPIDCHECK_HEADERS@ +EXCLUDE_PATTERNS = *_internal.h +GENERATE_TREEVIEW = YES +OPTIMIZE_OUTPUT_FOR_C = YES + +USE_MDFILE_AS_MAINPAGE = doc/external-api/README.md diff --git a/doc/external-api/local.mk b/doc/external-api/local.mk new file mode 100644 index 000000000..c739bdaf0 --- /dev/null +++ b/doc/external-api/local.mk @@ -0,0 +1,7 @@ +$(docdir)/external-api/html/index.html $(docdir)/external-api/latex: $(d)/doxygen.cfg + mkdir -p $(docdir)/external-api + { cat $< ; echo "OUTPUT_DIRECTORY=$(docdir)/external-api" ; } | doxygen - + +# Generate the HTML API docs for Nix's unstable C bindings +.PHONY: external-api-html +external-api-html: $(docdir)/external-api/html/index.html diff --git a/doc/manual/custom.css b/doc/manual/custom.css index b90f5423f..9e8e3886f 100644 --- a/doc/manual/custom.css +++ b/doc/manual/custom.css @@ -1,3 +1,25 @@ +:root { + --sidebar-width: 23em; +} + +h1.menu-title::before { + content: ""; + background-image: url("./favicon.svg"); + padding: 1.25em; + background-position: center center; + background-size: 2em; + background-repeat: no-repeat; +} + + +h1.menu-title { + padding: 0.5em; +} + +.sidebar .sidebar-scrollbox { + padding: 1em; +} + h1:not(:first-of-type) { margin-top: 1.3em; } diff --git a/doc/manual/local.mk b/doc/manual/local.mk index b77168885..71ad5c8e6 100644 --- a/doc/manual/local.mk +++ b/doc/manual/local.mk @@ -175,6 +175,16 @@ $(d)/src/SUMMARY-rl-next.md: $(d)/src/release-notes/rl-next.md # Generate the HTML manual. .PHONY: manual-html manual-html: $(docdir)/manual/index.html + +# Open the built HTML manual in the default browser. +manual-html-open: $(docdir)/manual/index.html + @echo " OPEN " $<; \ + xdg-open $< \ + || open $< \ + || { \ + echo "Could not open the manual in a browser. Please open '$<'" >&2; \ + false; \ + } install: $(docdir)/manual/index.html # Generate 'nix' manpages. @@ -207,7 +217,7 @@ doc/manual/generated/man1/nix3-manpages: $(d)/src/command-ref/new-cli # `@docroot@` is to be preserved for documenting the mechanism # FIXME: maybe contributing guides should live right next to the code # instead of in the manual -$(docdir)/manual/index.html: $(MANUAL_SRCS) $(d)/book.toml $(d)/anchors.jq $(d)/custom.css $(d)/src/SUMMARY.md $(d)/src/store/types $(d)/src/command-ref/new-cli $(d)/src/contributing/experimental-feature-descriptions.md $(d)/src/command-ref/conf-file.md $(d)/src/language/builtins.md $(d)/src/language/builtin-constants.md $(d)/src/release-notes/rl-next.md +$(docdir)/manual/index.html: $(MANUAL_SRCS) $(d)/book.toml $(d)/anchors.jq $(d)/custom.css $(d)/src/SUMMARY.md $(d)/src/store/types $(d)/src/command-ref/new-cli $(d)/src/contributing/experimental-feature-descriptions.md $(d)/src/command-ref/conf-file.md $(d)/src/language/builtins.md $(d)/src/language/builtin-constants.md $(d)/src/release-notes/rl-next.md $(d)/src/figures $(d)/src/favicon.png $(d)/src/favicon.svg $(trace-gen) \ tmp="$$(mktemp -d)"; \ cp -r doc/manual "$$tmp"; \ diff --git a/doc/manual/rl-next/remove-repl-flake.md b/doc/manual/rl-next/remove-repl-flake.md new file mode 100644 index 000000000..23298e2ed --- /dev/null +++ b/doc/manual/rl-next/remove-repl-flake.md @@ -0,0 +1,8 @@ +--- +synopsis: Remove experimental repl-flake +significance: significant +issues: 10103 +prs: 10299 +--- + +The `repl-flake` experimental feature has been removed. The `nix repl` command now works like the rest of the new CLI in that `nix repl {path}` now tries to load a flake at `{path}` (or fails if the `flakes` experimental feature isn't enabled).* diff --git a/doc/manual/src/SUMMARY.md.in b/doc/manual/src/SUMMARY.md.in index 1149fc7b4..43b9e925f 100644 --- a/doc/manual/src/SUMMARY.md.in +++ b/doc/manual/src/SUMMARY.md.in @@ -119,7 +119,7 @@ - [Experimental Features](contributing/experimental-features.md) - [CLI guideline](contributing/cli-guideline.md) - [C++ style guide](contributing/cxx.md) -- [Release Notes](release-notes/index.md) +- [Releases](release-notes/index.md) {{#include ./SUMMARY-rl-next.md}} - [Release 2.21 (2024-03-11)](release-notes/rl-2.21.md) - [Release 2.20 (2024-01-29)](release-notes/rl-2.20.md) diff --git a/doc/manual/src/contributing/documentation.md b/doc/manual/src/contributing/documentation.md index 46cca759d..88b0bdaa9 100644 --- a/doc/manual/src/contributing/documentation.md +++ b/doc/manual/src/contributing/documentation.md @@ -27,11 +27,9 @@ and open `./result-doc/share/doc/nix/manual/index.html`. To build the manual incrementally, [enter the development shell](./hacking.md) and run: ```console -make manual-html -j $NIX_BUILD_CORES +make manual-html-open -j $NIX_BUILD_CORES ``` -and open `./outputs/doc/share/doc/nix/manual/language/index.html`. - In order to reflect changes to the [Makefile for the manual], clear all generated files before re-building: [Makefile for the manual]: https://github.com/NixOS/nix/blob/master/doc/manual/local.mk diff --git a/doc/manual/src/favicon.png b/doc/manual/src/favicon.png new file mode 100644 index 000000000..1ed2b5fe0 Binary files /dev/null and b/doc/manual/src/favicon.png differ diff --git a/doc/manual/src/favicon.svg b/doc/manual/src/favicon.svg new file mode 100644 index 000000000..1d2a6e835 --- /dev/null +++ b/doc/manual/src/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/doc/manual/src/installation/upgrading.md b/doc/manual/src/installation/upgrading.md index 38edcdbc5..a433f1d30 100644 --- a/doc/manual/src/installation/upgrading.md +++ b/doc/manual/src/installation/upgrading.md @@ -28,7 +28,7 @@ $ sudo su ## macOS multi-user ```console -$ sudo nix-env --install --file '' --attr nix -I nixpkgs=channel:nixpkgs-unstable +$ sudo nix-env --install --file '' --attr nix cacert -I nixpkgs=channel:nixpkgs-unstable $ sudo launchctl remove org.nixos.nix-daemon $ sudo launchctl load /Library/LaunchDaemons/org.nixos.nix-daemon.plist ``` diff --git a/doc/manual/src/release-notes/index.md b/doc/manual/src/release-notes/index.md index cc805e631..d4e6292a6 100644 --- a/doc/manual/src/release-notes/index.md +++ b/doc/manual/src/release-notes/index.md @@ -1,12 +1,13 @@ # Nix Release Notes +The Nix release cycle is calendar-based as follows: + Nix has a release cycle of roughly 6 weeks. Notable changes and additions are announced in the release notes for each version. -Bugfixes can be backported on request to previous Nix releases. -We typically backport only as far back as the Nix version used in the latest NixOS release, which is announced in the [NixOS release notes](https://nixos.org/manual/nixos/stable/release-notes.html#ch-release-notes). - -Backports never skip releases. -If a feature is backported to version `x.y`, it must also be available in version `x.(y+1)`. -This ensures that upgrading from an older version with backports is still safe and no backported functionality will go missing. +The supported Nix versions are: +- The latest release +- The version used in the stable NixOS release, which is announced in the [NixOS release notes](https://nixos.org/manual/nixos/stable/release-notes.html#ch-release-notes). +Bugfixes and security issues are backported to every supported version. +Patch releases are published as needed. diff --git a/flake.nix b/flake.nix index 660527636..d33f7ef06 100644 --- a/flake.nix +++ b/flake.nix @@ -290,6 +290,13 @@ enableInternalAPIDocs = true; }; + # API docs for Nix's C bindings. + external-api-docs = nixpkgsFor.x86_64-linux.native.callPackage ./package.nix { + inherit fileset; + doBuild = false; + enableExternalAPIDocs = true; + }; + # System tests. tests = import ./tests/nixos { inherit lib nixpkgs nixpkgsFor; } // { diff --git a/local.mk b/local.mk index 3f3abb9f0..67ec35dcd 100644 --- a/local.mk +++ b/local.mk @@ -2,9 +2,14 @@ GLOBAL_CXXFLAGS += -Wno-deprecated-declarations -Werror=switch # Allow switch-enum to be overridden for files that do not support it, usually because of dependency headers. ERROR_SWITCH_ENUM = -Werror=switch-enum -$(foreach i, config.h $(wildcard src/lib*/*.hh), \ +$(foreach i, config.h $(wildcard src/lib*/*.hh) $(wildcard src/lib*/*.h $(filter-out %_internal.h, $(wildcard src/lib*c/*.h))), \ $(eval $(call install-file-in, $(i), $(includedir)/nix, 0644))) +ifdef HOST_UNIX + $(foreach i, $(wildcard src/lib*/unix/*.hh), \ + $(eval $(call install-file-in, $(i), $(includedir)/nix, 0644))) +endif + $(GCH): src/libutil/util.hh config.h -GCH_CXXFLAGS = -I src/libutil +GCH_CXXFLAGS = $(INCLUDE_libutil) diff --git a/package.nix b/package.nix index 4cfa813d2..fbf6c3a2d 100644 --- a/package.nix +++ b/package.nix @@ -5,6 +5,7 @@ , autoreconfHook , aws-sdk-cpp , boehmgc +, buildPackages , nlohmann_json , bison , boost @@ -75,7 +76,10 @@ # sounds so long as evaluation just takes places within short-lived # processes. (When the process exits, the memory is reclaimed; it is # only leaked *within* the process.) -, enableGC ? true +# +# Temporarily disabled on Windows because the `GC_throw_bad_alloc` +# symbol is missing during linking. +, enableGC ? !stdenv.hostPlatform.isWindows # Whether to enable Markdown rendering in the Nix binary. , enableMarkdown ? !stdenv.hostPlatform.isWindows @@ -88,9 +92,10 @@ # - readline , readlineFlavor ? if stdenv.hostPlatform.isWindows then "readline" else "editline" -# Whether to build the internal API docs, can be done separately from +# Whether to build the internal/external API docs, can be done separately from # everything else. , enableInternalAPIDocs ? false +, enableExternalAPIDocs ? false # Whether to install unit tests. This is useful when cross compiling # since we cannot run them natively during the build, but can do so @@ -179,6 +184,9 @@ in { ./doc/manual ] ++ lib.optionals enableInternalAPIDocs [ ./doc/internal-api + ] ++ lib.optionals enableExternalAPIDocs [ + ./doc/external-api + ] ++ lib.optionals (enableInternalAPIDocs || enableExternalAPIDocs) [ # Source might not be compiled, but still must be available # for Doxygen to gather comments. ./src @@ -196,7 +204,7 @@ in { ++ lib.optional doBuild "dev" # If we are doing just build or just docs, the one thing will use # "out". We only need additional outputs if we are doing both. - ++ lib.optional (doBuild && (enableManual || enableInternalAPIDocs)) "doc" + ++ lib.optional (doBuild && (enableManual || enableInternalAPIDocs || enableExternalAPIDocs)) "doc" ++ lib.optional installUnitTests "check"; nativeBuildInputs = [ @@ -218,7 +226,7 @@ in { ] ++ lib.optionals (doInstallCheck || enableManual) [ jq # Also for custom mdBook preprocessor. ] ++ lib.optional stdenv.hostPlatform.isLinux util-linux - ++ lib.optional enableInternalAPIDocs doxygen + ++ lib.optional (enableInternalAPIDocs || enableExternalAPIDocs) doxygen ; buildInputs = lib.optionals doBuild [ @@ -282,6 +290,7 @@ in { (lib.enableFeature buildUnitTests "unit-tests") (lib.enableFeature doInstallCheck "functional-tests") (lib.enableFeature enableInternalAPIDocs "internal-api-docs") + (lib.enableFeature enableExternalAPIDocs "external-api-docs") (lib.enableFeature enableManual "doc-gen") (lib.enableFeature enableGC "gc") (lib.enableFeature enableMarkdown "markdown") @@ -306,7 +315,8 @@ in { makeFlags = "profiledir=$(out)/etc/profile.d PRECOMPILE_HEADERS=1"; installTargets = lib.optional doBuild "install" - ++ lib.optional enableInternalAPIDocs "internal-api-html"; + ++ lib.optional enableInternalAPIDocs "internal-api-html" + ++ lib.optional enableExternalAPIDocs "external-api-html"; installFlags = "sysconfdir=$(out)/etc"; @@ -333,6 +343,16 @@ in { '' + lib.optionalString enableInternalAPIDocs '' mkdir -p ''${!outputDoc}/nix-support echo "doc internal-api-docs $out/share/doc/nix/internal-api/html" >> ''${!outputDoc}/nix-support/hydra-build-products + '' + + lib.optionalString enableExternalAPIDocs '' + mkdir -p ''${!outputDoc}/nix-support + echo "doc external-api-docs $out/share/doc/nix/external-api/html" >> ''${!outputDoc}/nix-support/hydra-build-products + ''; + + # So the check output gets links for DLLs in the out output. + preFixup = lib.optionalString (stdenv.hostPlatform.isWindows && builtins.elem "check" finalAttrs.outputs) '' + ln -s "$check/lib/"*.dll "$check/bin" + ln -s "$out/bin/"*.dll "$check/bin" ''; doInstallCheck = attrs.doInstallCheck; diff --git a/src/build-remote/build-remote.cc b/src/build-remote/build-remote.cc index 118468477..18eee830b 100644 --- a/src/build-remote/build-remote.cc +++ b/src/build-remote/build-remote.cc @@ -202,7 +202,7 @@ static int main_build_remote(int argc, char * * argv) else drvstr = ""; - auto error = HintFmt(errorText); + auto error = HintFmt::fromFormatString(errorText); error % drvstr % neededSystem diff --git a/src/libcmd/local.mk b/src/libcmd/local.mk index abb7459a7..9aa33a9d3 100644 --- a/src/libcmd/local.mk +++ b/src/libcmd/local.mk @@ -6,7 +6,7 @@ libcmd_DIR := $(d) libcmd_SOURCES := $(wildcard $(d)/*.cc) -libcmd_CXXFLAGS += -I src/libutil -I src/libstore -I src/libexpr -I src/libmain -I src/libfetchers +libcmd_CXXFLAGS += $(INCLUDE_libutil) $(INCLUDE_libstore) $(INCLUDE_libfetchers) $(INCLUDE_libexpr) $(INCLUDE_libmain) libcmd_LDFLAGS = $(EDITLINE_LIBS) $(LOWDOWN_LIBS) $(THREAD_LDFLAGS) diff --git a/src/libcmd/markdown.cc b/src/libcmd/markdown.cc index a4e3c5a77..d62ff0d96 100644 --- a/src/libcmd/markdown.cc +++ b/src/libcmd/markdown.cc @@ -50,7 +50,7 @@ std::string renderMarkdownToTerminal(std::string_view markdown) if (!rndr_res) throw Error("allocation error while rendering Markdown"); - return filterANSIEscapes(std::string(buf->data, buf->size), !shouldANSI()); + return filterANSIEscapes(std::string(buf->data, buf->size), !isTTY()); #else return std::string(markdown); #endif diff --git a/src/libcmd/repl.cc b/src/libcmd/repl.cc index 4a501e575..bec37babe 100644 --- a/src/libcmd/repl.cc +++ b/src/libcmd/repl.cc @@ -357,7 +357,7 @@ ProcessLineResult NixRepl::processLine(std::string line) if (line.empty()) return ProcessLineResult::PromptAgain; - _isInterrupted = false; + setInterrupted(false); std::string command, arg; diff --git a/src/libexpr-c/local.mk b/src/libexpr-c/local.mk new file mode 100644 index 000000000..51b02562e --- /dev/null +++ b/src/libexpr-c/local.mk @@ -0,0 +1,25 @@ +libraries += libexprc + +libexprc_NAME = libnixexprc + +libexprc_DIR := $(d) + +libexprc_SOURCES := \ + $(wildcard $(d)/*.cc) \ + +# Not just for this library itself, but also for downstream libraries using this library + +INCLUDE_libexprc := -I $(d) +libexprc_CXXFLAGS += $(INCLUDE_libutil) $(INCLUDE_libutilc) \ + $(INCLUDE_libfetchers) \ + $(INCLUDE_libstore) $(INCLUDE_libstorec) \ + $(INCLUDE_libexpr) $(INCLUDE_libexprc) + +libexprc_LIBS = libutil libutilc libstore libstorec libexpr + +libexprc_LDFLAGS += $(THREAD_LDFLAGS) + +$(eval $(call install-file-in, $(d)/nix-expr-c.pc, $(libdir)/pkgconfig, 0644)) + +libexprc_FORCE_INSTALL := 1 + diff --git a/src/libexpr-c/nix-expr-c.pc.in b/src/libexpr-c/nix-expr-c.pc.in new file mode 100644 index 000000000..06897064d --- /dev/null +++ b/src/libexpr-c/nix-expr-c.pc.in @@ -0,0 +1,10 @@ +prefix=@prefix@ +libdir=@libdir@ +includedir=@includedir@ + +Name: Nix +Description: Nix Language Evaluator - C API +Version: @PACKAGE_VERSION@ +Requires: nix-store-c +Libs: -L${libdir} -lnixexprc +Cflags: -I${includedir}/nix diff --git a/src/libexpr-c/nix_api_expr.cc b/src/libexpr-c/nix_api_expr.cc new file mode 100644 index 000000000..a5c03d5aa --- /dev/null +++ b/src/libexpr-c/nix_api_expr.cc @@ -0,0 +1,178 @@ +#include +#include +#include +#include + +#include "config.hh" +#include "eval.hh" +#include "globals.hh" +#include "util.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" + +#ifdef HAVE_BOEHMGC +#include +#define GC_INCLUDE_NEW 1 +#include "gc_cpp.h" +#endif + +nix_err nix_libexpr_init(nix_c_context * context) +{ + if (context) + context->last_err_code = NIX_OK; + { + auto ret = nix_libutil_init(context); + if (ret != NIX_OK) + return ret; + } + { + auto ret = nix_libstore_init(context); + if (ret != NIX_OK) + return ret; + } + try { + nix::initGC(); + } + NIXC_CATCH_ERRS +} + +nix_err nix_expr_eval_from_string( + nix_c_context * context, EvalState * state, const char * expr, const char * path, Value * value) +{ + if (context) + context->last_err_code = NIX_OK; + try { + nix::Expr * parsedExpr = state->state.parseExprFromString(expr, state->state.rootPath(nix::CanonPath(path))); + state->state.eval(parsedExpr, *(nix::Value *) value); + state->state.forceValue(*(nix::Value *) value, nix::noPos); + } + NIXC_CATCH_ERRS +} + +nix_err nix_value_call(nix_c_context * context, EvalState * state, Value * fn, Value * arg, Value * value) +{ + if (context) + context->last_err_code = NIX_OK; + try { + state->state.callFunction(*(nix::Value *) fn, *(nix::Value *) arg, *(nix::Value *) value, nix::noPos); + state->state.forceValue(*(nix::Value *) value, nix::noPos); + } + NIXC_CATCH_ERRS +} + +nix_err nix_value_force(nix_c_context * context, EvalState * state, Value * value) +{ + if (context) + context->last_err_code = NIX_OK; + try { + state->state.forceValue(*(nix::Value *) value, nix::noPos); + } + NIXC_CATCH_ERRS +} + +nix_err nix_value_force_deep(nix_c_context * context, EvalState * state, Value * value) +{ + if (context) + context->last_err_code = NIX_OK; + try { + state->state.forceValueDeep(*(nix::Value *) value); + } + NIXC_CATCH_ERRS +} + +EvalState * nix_state_create(nix_c_context * context, const char ** searchPath_c, Store * store) +{ + if (context) + context->last_err_code = NIX_OK; + try { + nix::Strings searchPath; + if (searchPath_c != nullptr) + for (size_t i = 0; searchPath_c[i] != nullptr; i++) + searchPath.push_back(searchPath_c[i]); + + return new EvalState{nix::EvalState(nix::SearchPath::parse(searchPath), store->ptr)}; + } + NIXC_CATCH_ERRS_NULL +} + +void nix_state_free(EvalState * state) +{ + delete state; +} + +#ifdef HAVE_BOEHMGC +std::unordered_map< + const void *, + unsigned int, + std::hash, + std::equal_to, + traceable_allocator>> + nix_refcounts; + +std::mutex nix_refcount_lock; + +nix_err nix_gc_incref(nix_c_context * context, const void * p) +{ + if (context) + context->last_err_code = NIX_OK; + try { + std::scoped_lock lock(nix_refcount_lock); + auto f = nix_refcounts.find(p); + if (f != nix_refcounts.end()) { + f->second++; + } else { + nix_refcounts[p] = 1; + } + } + NIXC_CATCH_ERRS +} + +nix_err nix_gc_decref(nix_c_context * context, const void * p) +{ + + if (context) + context->last_err_code = NIX_OK; + try { + std::scoped_lock lock(nix_refcount_lock); + auto f = nix_refcounts.find(p); + if (f != nix_refcounts.end()) { + if (--f->second == 0) + nix_refcounts.erase(f); + } else + throw std::runtime_error("nix_gc_decref: object was not referenced"); + } + NIXC_CATCH_ERRS +} + +void nix_gc_now() +{ + GC_gcollect(); +} + +#else +nix_err nix_gc_incref(nix_c_context * context, const void *) +{ + if (context) + context->last_err_code = NIX_OK; + return NIX_OK; +} +nix_err nix_gc_decref(nix_c_context * context, const void *) +{ + if (context) + context->last_err_code = NIX_OK; + return NIX_OK; +} +void nix_gc_now() {} +#endif + +void nix_gc_register_finalizer(void * obj, void * cd, void (*finalizer)(void * obj, void * cd)) +{ +#ifdef HAVE_BOEHMGC + GC_REGISTER_FINALIZER(obj, finalizer, cd, 0, 0); +#endif +} diff --git a/src/libexpr-c/nix_api_expr.h b/src/libexpr-c/nix_api_expr.h new file mode 100644 index 000000000..7504b5d7a --- /dev/null +++ b/src/libexpr-c/nix_api_expr.h @@ -0,0 +1,213 @@ +#ifndef NIX_API_EXPR_H +#define NIX_API_EXPR_H +/** @defgroup libexpr libexpr + * @brief Bindings to the Nix language evaluator + * + * Example (without error handling): + * @code{.c} + * int main() { + * nix_libexpr_init(NULL); + * + * Store* store = nix_store_open(NULL, "dummy", NULL); + * EvalState* state = nix_state_create(NULL, NULL, store); // empty nix path + * Value *value = nix_alloc_value(NULL, state); + * + * nix_expr_eval_from_string(NULL, state, "builtins.nixVersion", ".", value); + * nix_value_force(NULL, state, value); + * printf("nix version: %s\n", nix_get_string(NULL, value)); + * + * nix_gc_decref(NULL, value); + * nix_state_free(state); + * nix_store_free(store); + * return 0; + * } + * @endcode + * @{ + */ +/** @file + * @brief Main entry for the libexpr C bindings + */ + +#include "nix_api_store.h" +#include "nix_api_util.h" + +#ifdef __cplusplus +extern "C" { +#endif +// cffi start + +// Type definitions +/** + * @brief Represents a state of the Nix language evaluator. + * + * Multiple states can be created for multi-threaded + * operation. + * @struct EvalState + * @see nix_state_create + */ +typedef struct EvalState EvalState; // nix::EvalState +/** + * @brief Represents a value in the Nix language. + * + * Owned by the garbage collector. + * @struct Value + * @see value_manip + */ +typedef void Value; // nix::Value + +// Function prototypes +/** + * @brief Initialize the Nix language evaluator. + * + * This function must be called at least once, + * at some point before constructing a EvalState for the first time. + * This function can be called multiple times, and is idempotent. + * + * @param[out] context Optional, stores error information + * @return NIX_OK if the initialization was successful, an error code otherwise. + */ +nix_err nix_libexpr_init(nix_c_context * context); + +/** + * @brief Parses and evaluates a Nix expression from a string. + * + * @param[out] context Optional, stores error information + * @param[in] state The state of the evaluation. + * @param[in] expr The Nix expression to parse. + * @param[in] path The file path to associate with the expression. + * This is required for expressions that contain relative paths (such as `./.`) that are resolved relative to the given + * directory. + * @param[out] value The result of the evaluation. You must allocate this + * yourself. + * @return NIX_OK if the evaluation was successful, an error code otherwise. + */ +nix_err nix_expr_eval_from_string( + nix_c_context * context, EvalState * state, const char * expr, const char * path, Value * value); + +/** + * @brief Calls a Nix function with an argument. + * + * @param[out] context Optional, stores error information + * @param[in] state The state of the evaluation. + * @param[in] fn The Nix function to call. + * @param[in] arg The argument to pass to the function. + * @param[out] value The result of the function call. + * @return NIX_OK if the function call was successful, an error code otherwise. + */ +nix_err nix_value_call(nix_c_context * context, EvalState * state, Value * fn, Value * arg, Value * value); + +/** + * @brief Forces the evaluation of a Nix value. + * + * The Nix interpreter is lazy, and not-yet-evaluated Values can be + * of type NIX_TYPE_THUNK instead of their actual value. + * + * This function converts these Values into their final type. + * + * @note You don't need this function for basic API usage, since all functions + * that return a value call it for you. The only place you will see a + * NIX_TYPE_THUNK is in the arguments that are passed to a PrimOp function + * you supplied to nix_alloc_primop. + * + * @param[out] context Optional, stores error information + * @param[in] state The state of the evaluation. + * @param[in,out] value The Nix value to force. + * @post value is not of type NIX_TYPE_THUNK + * @return NIX_OK if the force operation was successful, an error code + * otherwise. + */ +nix_err nix_value_force(nix_c_context * context, EvalState * state, Value * value); + +/** + * @brief Forces the deep evaluation of a Nix value. + * + * Recursively calls nix_value_force + * + * @see nix_value_force + * @warning Calling this function on a recursive data structure will cause a + * stack overflow. + * @param[out] context Optional, stores error information + * @param[in] state The state of the evaluation. + * @param[in,out] value The Nix value to force. + * @return NIX_OK if the deep force operation was successful, an error code + * otherwise. + */ +nix_err nix_value_force_deep(nix_c_context * context, EvalState * state, Value * value); + +/** + * @brief Create a new Nix language evaluator state. + * + * @param[out] context Optional, stores error information + * @param[in] searchPath Array of strings corresponding to entries in NIX_PATH. + * @param[in] store The Nix store to use. + * @return A new Nix state or NULL on failure. + */ +EvalState * nix_state_create(nix_c_context * context, const char ** searchPath, Store * store); + +/** + * @brief Frees a Nix state. + * + * Does not fail. + * + * @param[in] state The state to free. + */ +void nix_state_free(EvalState * state); + +/** @addtogroup GC + * @brief Reference counting and garbage collector operations + * + * The Nix language evaluator uses a garbage collector. To ease C interop, we implement + * a reference counting scheme, where objects will be deallocated + * when there are no references from the Nix side, and the reference count kept + * by the C API reaches `0`. + * + * Functions returning a garbage-collected object will automatically increase + * the refcount for you. You should make sure to call `nix_gc_decref` when + * you're done with a value returned by the evaluator. + * @{ + */ +/** + * @brief Increment the garbage collector reference counter for the given object. + * + * The Nix language evaluator C API keeps track of alive objects by reference counting. + * When you're done with a refcounted pointer, call nix_gc_decref(). + * + * @param[out] context Optional, stores error information + * @param[in] object The object to keep alive + */ +nix_err nix_gc_incref(nix_c_context * context, const void * object); +/** + * @brief Decrement the garbage collector reference counter for the given object + * + * @param[out] context Optional, stores error information + * @param[in] object The object to stop referencing + */ +nix_err nix_gc_decref(nix_c_context * context, const void * object); + +/** + * @brief Trigger the garbage collector manually + * + * You should not need to do this, but it can be useful for debugging. + */ +void nix_gc_now(); + +/** + * @brief Register a callback that gets called when the object is garbage + * collected. + * @note Objects can only have a single finalizer. This function overwrites existing values + * silently. + * @param[in] obj the object to watch + * @param[in] cd the data to pass to the finalizer + * @param[in] finalizer the callback function, called with obj and cd + */ +void nix_gc_register_finalizer(void * obj, void * cd, void (*finalizer)(void * obj, void * cd)); + +/** @} */ +// cffi end +#ifdef __cplusplus +} +#endif + +/** @} */ + +#endif // NIX_API_EXPR_H diff --git a/src/libexpr-c/nix_api_expr_internal.h b/src/libexpr-c/nix_api_expr_internal.h new file mode 100644 index 000000000..b50a51347 --- /dev/null +++ b/src/libexpr-c/nix_api_expr_internal.h @@ -0,0 +1,38 @@ +#ifndef NIX_API_EXPR_INTERNAL_H +#define NIX_API_EXPR_INTERNAL_H + +#include "eval.hh" +#include "attr-set.hh" +#include "nix_api_value.h" + +struct EvalState +{ + nix::EvalState state; +}; + +struct BindingsBuilder +{ + nix::BindingsBuilder builder; +}; + +struct ListBuilder +{ + nix::ListBuilder builder; +}; + +struct nix_string_return +{ + std::string str; +}; + +struct nix_printer +{ + std::ostream & s; +}; + +struct nix_string_context +{ + nix::NixStringContext & ctx; +}; + +#endif // NIX_API_EXPR_INTERNAL_H diff --git a/src/libexpr-c/nix_api_external.cc b/src/libexpr-c/nix_api_external.cc new file mode 100644 index 000000000..3c3dd6ca9 --- /dev/null +++ b/src/libexpr-c/nix_api_external.cc @@ -0,0 +1,198 @@ +#include "attr-set.hh" +#include "config.hh" +#include "eval.hh" +#include "globals.hh" +#include "value.hh" + +#include "nix_api_expr.h" +#include "nix_api_expr_internal.h" +#include "nix_api_external.h" +#include "nix_api_util.h" +#include "nix_api_util_internal.h" +#include "nix_api_value.h" +#include "value/context.hh" + +#include + +#ifdef HAVE_BOEHMGC +# include "gc/gc.h" +# define GC_INCLUDE_NEW 1 +# include "gc_cpp.h" +#endif + +void nix_set_string_return(nix_string_return * str, const char * c) +{ + str->str = c; +} + +nix_err nix_external_print(nix_c_context * context, nix_printer * printer, const char * c) +{ + if (context) + context->last_err_code = NIX_OK; + try { + printer->s << c; + } + NIXC_CATCH_ERRS +} + +nix_err nix_external_add_string_context(nix_c_context * context, nix_string_context * ctx, const char * c) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto r = nix::NixStringContextElem::parse(c); + ctx->ctx.insert(r); + } + NIXC_CATCH_ERRS +} + +class NixCExternalValue : public nix::ExternalValueBase +{ + NixCExternalValueDesc & desc; + void * v; + +public: + NixCExternalValue(NixCExternalValueDesc & desc, void * v) + : desc(desc) + , v(v){}; + void * get_ptr() + { + return v; + } + /** + * Print out the value + */ + virtual std::ostream & print(std::ostream & str) const override + { + nix_printer p{str}; + desc.print(v, &p); + return str; + } + + /** + * Return a simple string describing the type + */ + virtual std::string showType() const override + { + nix_string_return res; + desc.showType(v, &res); + return std::move(res.str); + } + + /** + * Return a string to be used in builtins.typeOf + */ + virtual std::string typeOf() const override + { + nix_string_return res; + desc.typeOf(v, &res); + return std::move(res.str); + } + + /** + * Coerce the value to a string. + */ + virtual std::string coerceToString( + nix::EvalState & state, + const nix::PosIdx & pos, + nix::NixStringContext & context, + bool copyMore, + bool copyToStore) const override + { + if (!desc.coerceToString) { + return nix::ExternalValueBase::coerceToString(state, pos, context, copyMore, copyToStore); + } + nix_string_context ctx{context}; + nix_string_return res{""}; + // todo: pos, errors + desc.coerceToString(v, &ctx, copyMore, copyToStore, &res); + if (res.str.empty()) { + return nix::ExternalValueBase::coerceToString(state, pos, context, copyMore, copyToStore); + } + return std::move(res.str); + } + + /** + * Compare to another value of the same type. + */ + virtual bool operator==(const ExternalValueBase & b) const override + { + if (!desc.equal) { + return false; + } + auto r = dynamic_cast(&b); + if (!r) + return false; + return desc.equal(v, r->v); + } + + /** + * Print the value as JSON. + */ + virtual nlohmann::json printValueAsJSON( + nix::EvalState & state, bool strict, nix::NixStringContext & context, bool copyToStore = true) const override + { + if (!desc.printValueAsJSON) { + return nix::ExternalValueBase::printValueAsJSON(state, strict, context, copyToStore); + } + nix_string_context ctx{context}; + nix_string_return res{""}; + desc.printValueAsJSON(v, (EvalState *) &state, strict, &ctx, copyToStore, &res); + if (res.str.empty()) { + return nix::ExternalValueBase::printValueAsJSON(state, strict, context, copyToStore); + } + return nlohmann::json::parse(res.str); + } + + /** + * Print the value as XML. + */ + virtual void printValueAsXML( + nix::EvalState & state, + bool strict, + bool location, + nix::XMLWriter & doc, + nix::NixStringContext & context, + nix::PathSet & drvsSeen, + const nix::PosIdx pos) const override + { + if (!desc.printValueAsXML) { + return nix::ExternalValueBase::printValueAsXML(state, strict, location, doc, context, drvsSeen, pos); + } + nix_string_context ctx{context}; + desc.printValueAsXML( + v, (EvalState *) &state, strict, location, &doc, &ctx, &drvsSeen, + *reinterpret_cast(&pos)); + } + + virtual ~NixCExternalValue() override{}; +}; + +ExternalValue * nix_create_external_value(nix_c_context * context, NixCExternalValueDesc * desc, void * v) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto ret = new +#ifdef HAVE_BOEHMGC + (GC) +#endif + NixCExternalValue(*desc, v); + nix_gc_incref(nullptr, ret); + return (ExternalValue *) ret; + } + NIXC_CATCH_ERRS_NULL +} + +void * nix_get_external_value_content(nix_c_context * context, ExternalValue * b) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto r = dynamic_cast((nix::ExternalValueBase *) b); + if (r) + return r->get_ptr(); + return nullptr; + } + NIXC_CATCH_ERRS_NULL +} diff --git a/src/libexpr-c/nix_api_external.h b/src/libexpr-c/nix_api_external.h new file mode 100644 index 000000000..12ea00407 --- /dev/null +++ b/src/libexpr-c/nix_api_external.h @@ -0,0 +1,196 @@ +#ifndef NIX_API_EXTERNAL_H +#define NIX_API_EXTERNAL_H +/** @ingroup libexpr + * @addtogroup Externals + * @brief Deal with external values + * @{ + */ +/** @file + * @brief libexpr C bindings dealing with external values + */ + +#include "nix_api_expr.h" +#include "nix_api_util.h" +#include "nix_api_value.h" +#include "stdbool.h" +#include "stddef.h" +#include "stdint.h" + +#ifdef __cplusplus +extern "C" { +#endif +// cffi start + +/** + * @brief Represents a string owned by the Nix language evaluator. + * @see nix_set_owned_string + */ +typedef struct nix_string_return nix_string_return; +/** + * @brief Wraps a stream that can output multiple string pieces. + */ +typedef struct nix_printer nix_printer; +/** + * @brief A list of string context items + */ +typedef struct nix_string_context nix_string_context; + +/** + * @brief Sets the contents of a nix_string_return + * + * Copies the passed string. + * @param[out] str the nix_string_return to write to + * @param[in] c The string to copy + */ +void nix_set_string_return(nix_string_return * str, const char * c); + +/** + * Print to the nix_printer + * + * @param[out] context Optional, stores error information + * @param printer The nix_printer to print to + * @param[in] str The string to print + * @returns NIX_OK if everything worked + */ +nix_err nix_external_print(nix_c_context * context, nix_printer * printer, const char * str); + +/** + * Add string context to the nix_string_context object + * @param[out] context Optional, stores error information + * @param[out] string_context The nix_string_context to add to + * @param[in] c The context string to add + * @returns NIX_OK if everything worked + */ +nix_err nix_external_add_string_context(nix_c_context * context, nix_string_context * string_context, const char * c); + +/** + * @brief Definition for a class of external values + * + * Create and implement one of these, then pass it to nix_create_external_value + * Make sure to keep it alive while the external value lives. + * + * Optional functions can be set to NULL + * + * @see nix_create_external_value + */ +typedef struct NixCExternalValueDesc +{ + /** + * @brief Called when printing the external value + * + * @param[in] self the void* passed to nix_create_external_value + * @param[out] printer The printer to print to, pass to nix_external_print + */ + void (*print)(void * self, nix_printer * printer); + /** + * @brief Called on :t + * @param[in] self the void* passed to nix_create_external_value + * @param[out] res the return value + */ + void (*showType)(void * self, nix_string_return * res); + /** + * @brief Called on `builtins.typeOf` + * @param self the void* passed to nix_create_external_value + * @param[out] res the return value + */ + void (*typeOf)(void * self, nix_string_return * res); + /** + * @brief Called on "${str}" and builtins.toString. + * + * The latter with coerceMore=true + * Optional, the default is to throw an error. + * @param[in] self the void* passed to nix_create_external_value + * @param[out] c writable string context for the resulting string + * @param[in] coerceMore boolean, try to coerce to strings in more cases + * instead of throwing an error + * @param[in] copyToStore boolean, whether to copy referenced paths to store + * or keep them as-is + * @param[out] res the return value. Not touching this, or setting it to the + * empty string, will make the conversion throw an error. + */ + void (*coerceToString)( + void * self, nix_string_context * c, int coerceMore, int copyToStore, nix_string_return * res); + /** + * @brief Try to compare two external values + * + * Optional, the default is always false. + * If the other object was not a Nix C external value, this comparison will + * also return false + * @param[in] self the void* passed to nix_create_external_value + * @param[in] other the void* passed to the other object's + * nix_create_external_value + * @returns true if the objects are deemed to be equal + */ + int (*equal)(void * self, void * other); + /** + * @brief Convert the external value to json + * + * Optional, the default is to throw an error + * @param[in] self the void* passed to nix_create_external_value + * @param[in] state The evaluator state + * @param[in] strict boolean Whether to force the value before printing + * @param[out] c writable string context for the resulting string + * @param[in] copyToStore whether to copy referenced paths to store or keep + * them as-is + * @param[out] res the return value. Gets parsed as JSON. Not touching this, + * or setting it to the empty string, will make the conversion throw an error. + */ + void (*printValueAsJSON)( + void * self, EvalState *, bool strict, nix_string_context * c, bool copyToStore, nix_string_return * res); + /** + * @brief Convert the external value to XML + * + * Optional, the default is to throw an error + * @todo The mechanisms for this call are incomplete. There are no C + * bindings to work with XML, pathsets and positions. + * @param[in] self the void* passed to nix_create_external_value + * @param[in] state The evaluator state + * @param[in] strict boolean Whether to force the value before printing + * @param[in] location boolean Whether to include position information in the + * xml + * @param[out] doc XML document to output to + * @param[out] c writable string context for the resulting string + * @param[in,out] drvsSeen a path set to avoid duplicating derivations + * @param[in] pos The position of the call. + */ + void (*printValueAsXML)( + void * self, + EvalState *, + int strict, + int location, + void * doc, + nix_string_context * c, + void * drvsSeen, + int pos); +} NixCExternalValueDesc; + +/** + * @brief Create an external value, that can be given to nix_init_external + * + * Owned by the GC. Use nix_gc_decref when you're done with the pointer. + * + * @param[out] context Optional, stores error information + * @param[in] desc a NixCExternalValueDesc, you should keep this alive as long + * as the ExternalValue lives + * @param[in] v the value to store + * @returns external value, owned by the garbage collector + * @see nix_init_external + */ +ExternalValue * nix_create_external_value(nix_c_context * context, NixCExternalValueDesc * desc, void * v); + +/** + * @brief Extract the pointer from a nix c external value. + * @param[out] context Optional, stores error information + * @param[in] b The external value + * @returns The pointer, or null if the external value was not from nix c. + * @see nix_get_external + */ +void * nix_get_external_value_content(nix_c_context * context, ExternalValue * b); + +// cffi end +#ifdef __cplusplus +} +#endif +/** @} */ + +#endif // NIX_API_EXTERNAL_H diff --git a/src/libexpr-c/nix_api_value.cc b/src/libexpr-c/nix_api_value.cc new file mode 100644 index 000000000..02bd154b3 --- /dev/null +++ b/src/libexpr-c/nix_api_value.cc @@ -0,0 +1,530 @@ +#include "attr-set.hh" +#include "config.hh" +#include "eval.hh" +#include "globals.hh" +#include "primops.hh" +#include "value.hh" + +#include "nix_api_expr.h" +#include "nix_api_expr_internal.h" +#include "nix_api_util.h" +#include "nix_api_util_internal.h" +#include "nix_api_value.h" + +#ifdef HAVE_BOEHMGC +# include "gc/gc.h" +# define GC_INCLUDE_NEW 1 +# include "gc_cpp.h" +#endif + +// Helper function to throw an exception if value is null +static const nix::Value & check_value_not_null(const Value * value) +{ + if (!value) { + throw std::runtime_error("Value is null"); + } + return *((const nix::Value *) value); +} + +static nix::Value & check_value_not_null(Value * value) +{ + if (!value) { + throw std::runtime_error("Value is null"); + } + return *((nix::Value *) value); +} + +/** + * Helper function to convert calls from nix into C API. + * + * Deals with errors and converts arguments from C++ into C types. + */ +static void nix_c_primop_wrapper( + PrimOpFun f, void * userdata, nix::EvalState & state, const nix::PosIdx pos, nix::Value ** args, nix::Value & v) +{ + nix_c_context ctx; + f(userdata, &ctx, (EvalState *) &state, (Value **) args, (Value *) &v); + /* TODO: In the future, this should throw different errors depending on the error code */ + if (ctx.last_err_code != NIX_OK) + state.error("Error from builtin function: %s", *ctx.last_err).atPos(pos).debugThrow(); +} + +PrimOp * nix_alloc_primop( + nix_c_context * context, + PrimOpFun fun, + int arity, + const char * name, + const char ** args, + const char * doc, + void * user_data) +{ + if (context) + context->last_err_code = NIX_OK; + try { + using namespace std::placeholders; + auto p = new +#ifdef HAVE_BOEHMGC + (GC) +#endif + nix::PrimOp{ + .name = name, + .args = {}, + .arity = (size_t) arity, + .doc = doc, + .fun = std::bind(nix_c_primop_wrapper, fun, user_data, _1, _2, _3, _4)}; + if (args) + for (size_t i = 0; args[i]; i++) + p->args.emplace_back(*args); + nix_gc_incref(nullptr, p); + return (PrimOp *) p; + } + NIXC_CATCH_ERRS_NULL +} + +nix_err nix_register_primop(nix_c_context * context, PrimOp * primOp) +{ + if (context) + context->last_err_code = NIX_OK; + try { + nix::RegisterPrimOp r(std::move(*((nix::PrimOp *) primOp))); + } + NIXC_CATCH_ERRS +} + +Value * nix_alloc_value(nix_c_context * context, EvalState * state) +{ + if (context) + context->last_err_code = NIX_OK; + try { + Value * res = state->state.allocValue(); + nix_gc_incref(nullptr, res); + return res; + } + NIXC_CATCH_ERRS_NULL +} + +ValueType nix_get_type(nix_c_context * context, const Value * value) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto & v = check_value_not_null(value); + using namespace nix; + switch (v.type()) { + case nThunk: + return NIX_TYPE_THUNK; + case nInt: + return NIX_TYPE_INT; + case nFloat: + return NIX_TYPE_FLOAT; + case nBool: + return NIX_TYPE_BOOL; + case nString: + return NIX_TYPE_STRING; + case nPath: + return NIX_TYPE_PATH; + case nNull: + return NIX_TYPE_NULL; + case nAttrs: + return NIX_TYPE_ATTRS; + case nList: + return NIX_TYPE_LIST; + case nFunction: + return NIX_TYPE_FUNCTION; + case nExternal: + return NIX_TYPE_EXTERNAL; + } + return NIX_TYPE_NULL; + } + NIXC_CATCH_ERRS_RES(NIX_TYPE_NULL); +} + +const char * nix_get_typename(nix_c_context * context, const Value * value) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto & v = check_value_not_null(value); + auto s = nix::showType(v); + return strdup(s.c_str()); + } + NIXC_CATCH_ERRS_NULL +} + +bool nix_get_bool(nix_c_context * context, const Value * value) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto & v = check_value_not_null(value); + assert(v.type() == nix::nBool); + return v.boolean; + } + NIXC_CATCH_ERRS_RES(false); +} + +const char * nix_get_string(nix_c_context * context, const Value * value) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto & v = check_value_not_null(value); + assert(v.type() == nix::nString); + return v.c_str(); + } + NIXC_CATCH_ERRS_NULL +} + +const char * nix_get_path_string(nix_c_context * context, const Value * value) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto & v = check_value_not_null(value); + assert(v.type() == nix::nPath); + // NOTE (from @yorickvP) + // v._path.path should work but may not be how Eelco intended it. + // Long-term this function should be rewritten to copy some data into a + // user-allocated string. + // We could use v.path().to_string().c_str(), but I'm concerned this + // crashes. Looks like .path() allocates a CanonPath with a copy of the + // string, then it gets the underlying data from that. + return v._path.path; + } + NIXC_CATCH_ERRS_NULL +} + +unsigned int nix_get_list_size(nix_c_context * context, const Value * value) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto & v = check_value_not_null(value); + assert(v.type() == nix::nList); + return v.listSize(); + } + NIXC_CATCH_ERRS_RES(0); +} + +unsigned int nix_get_attrs_size(nix_c_context * context, const Value * value) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto & v = check_value_not_null(value); + assert(v.type() == nix::nAttrs); + return v.attrs->size(); + } + NIXC_CATCH_ERRS_RES(0); +} + +double nix_get_float(nix_c_context * context, const Value * value) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto & v = check_value_not_null(value); + assert(v.type() == nix::nFloat); + return v.fpoint; + } + NIXC_CATCH_ERRS_RES(0.0); +} + +int64_t nix_get_int(nix_c_context * context, const Value * value) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto & v = check_value_not_null(value); + assert(v.type() == nix::nInt); + return v.integer; + } + NIXC_CATCH_ERRS_RES(0); +} + +ExternalValue * nix_get_external(nix_c_context * context, Value * value) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto & v = check_value_not_null(value); + assert(v.type() == nix::nExternal); + return (ExternalValue *) v.external; + } + NIXC_CATCH_ERRS_NULL; +} + +Value * nix_get_list_byidx(nix_c_context * context, const Value * value, EvalState * state, unsigned int ix) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto & v = check_value_not_null(value); + assert(v.type() == nix::nList); + auto * p = v.listElems()[ix]; + nix_gc_incref(nullptr, p); + if (p != nullptr) + state->state.forceValue(*p, nix::noPos); + return (Value *) p; + } + NIXC_CATCH_ERRS_NULL +} + +Value * nix_get_attr_byname(nix_c_context * context, const Value * value, EvalState * state, const char * name) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto & v = check_value_not_null(value); + assert(v.type() == nix::nAttrs); + nix::Symbol s = state->state.symbols.create(name); + auto attr = v.attrs->get(s); + if (attr) { + nix_gc_incref(nullptr, attr->value); + state->state.forceValue(*attr->value, nix::noPos); + return attr->value; + } + nix_set_err_msg(context, NIX_ERR_KEY, "missing attribute"); + return nullptr; + } + NIXC_CATCH_ERRS_NULL +} + +bool nix_has_attr_byname(nix_c_context * context, const Value * value, EvalState * state, const char * name) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto & v = check_value_not_null(value); + assert(v.type() == nix::nAttrs); + nix::Symbol s = state->state.symbols.create(name); + auto attr = v.attrs->get(s); + if (attr) + return true; + return false; + } + NIXC_CATCH_ERRS_RES(false); +} + +Value * +nix_get_attr_byidx(nix_c_context * context, const Value * value, EvalState * state, unsigned int i, const char ** name) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto & v = check_value_not_null(value); + const nix::Attr & a = (*v.attrs)[i]; + *name = ((const std::string &) (state->state.symbols[a.name])).c_str(); + nix_gc_incref(nullptr, a.value); + state->state.forceValue(*a.value, nix::noPos); + return a.value; + } + NIXC_CATCH_ERRS_NULL +} + +const char * nix_get_attr_name_byidx(nix_c_context * context, const Value * value, EvalState * state, unsigned int i) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto & v = check_value_not_null(value); + const nix::Attr & a = (*v.attrs)[i]; + return ((const std::string &) (state->state.symbols[a.name])).c_str(); + } + NIXC_CATCH_ERRS_NULL +} + +nix_err nix_init_bool(nix_c_context * context, Value * value, bool b) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto & v = check_value_not_null(value); + v.mkBool(b); + } + NIXC_CATCH_ERRS +} + +// todo string context +nix_err nix_init_string(nix_c_context * context, Value * value, const char * str) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto & v = check_value_not_null(value); + v.mkString(std::string_view(str)); + } + NIXC_CATCH_ERRS +} + +nix_err nix_init_path_string(nix_c_context * context, EvalState * s, Value * value, const char * str) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto & v = check_value_not_null(value); + v.mkPath(s->state.rootPath(nix::CanonPath(str))); + } + NIXC_CATCH_ERRS +} + +nix_err nix_init_float(nix_c_context * context, Value * value, double d) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto & v = check_value_not_null(value); + v.mkFloat(d); + } + NIXC_CATCH_ERRS +} + +nix_err nix_init_int(nix_c_context * context, Value * value, int64_t i) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto & v = check_value_not_null(value); + v.mkInt(i); + } + NIXC_CATCH_ERRS +} + +nix_err nix_init_null(nix_c_context * context, Value * value) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto & v = check_value_not_null(value); + v.mkNull(); + } + NIXC_CATCH_ERRS +} + +nix_err nix_init_external(nix_c_context * context, Value * value, ExternalValue * val) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto & v = check_value_not_null(value); + auto r = (nix::ExternalValueBase *) val; + v.mkExternal(r); + } + NIXC_CATCH_ERRS +} + +ListBuilder * nix_make_list_builder(nix_c_context * context, EvalState * state, size_t capacity) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto builder = state->state.buildList(capacity); + return new +#if HAVE_BOEHMGC + (NoGC) +#endif + ListBuilder{std::move(builder)}; + } + NIXC_CATCH_ERRS_NULL +} + +nix_err nix_list_builder_insert(nix_c_context * context, ListBuilder * list_builder, unsigned int index, Value * value) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto & e = check_value_not_null(value); + list_builder->builder[index] = &e; + } + NIXC_CATCH_ERRS +} + +void nix_list_builder_free(ListBuilder * list_builder) +{ +#if HAVE_BOEHMGC + GC_FREE(list_builder); +#else + delete list_builder; +#endif +} + +nix_err nix_make_list(nix_c_context * context, ListBuilder * list_builder, Value * value) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto & v = check_value_not_null(value); + v.mkList(list_builder->builder); + } + NIXC_CATCH_ERRS +} + +nix_err nix_init_primop(nix_c_context * context, Value * value, PrimOp * p) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto & v = check_value_not_null(value); + v.mkPrimOp((nix::PrimOp *) p); + } + NIXC_CATCH_ERRS +} + +nix_err nix_copy_value(nix_c_context * context, Value * value, Value * source) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto & v = check_value_not_null(value); + auto & s = check_value_not_null(source); + v = s; + } + NIXC_CATCH_ERRS +} + +nix_err nix_make_attrs(nix_c_context * context, Value * value, BindingsBuilder * b) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto & v = check_value_not_null(value); + v.mkAttrs(b->builder); + } + NIXC_CATCH_ERRS +} + +BindingsBuilder * nix_make_bindings_builder(nix_c_context * context, EvalState * state, size_t capacity) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto bb = state->state.buildBindings(capacity); + return new +#if HAVE_BOEHMGC + (NoGC) +#endif + BindingsBuilder{std::move(bb)}; + } + NIXC_CATCH_ERRS_NULL +} + +nix_err nix_bindings_builder_insert(nix_c_context * context, BindingsBuilder * bb, const char * name, Value * value) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto & v = check_value_not_null(value); + nix::Symbol s = bb->builder.state.symbols.create(name); + bb->builder.insert(s, &v); + } + NIXC_CATCH_ERRS +} + +void nix_bindings_builder_free(BindingsBuilder * bb) +{ +#if HAVE_BOEHMGC + GC_FREE((nix::BindingsBuilder *) bb); +#else + delete (nix::BindingsBuilder *) bb; +#endif +} diff --git a/src/libexpr-c/nix_api_value.h b/src/libexpr-c/nix_api_value.h new file mode 100644 index 000000000..42218188c --- /dev/null +++ b/src/libexpr-c/nix_api_value.h @@ -0,0 +1,434 @@ +#ifndef NIX_API_VALUE_H +#define NIX_API_VALUE_H + +/** @addtogroup libexpr + * @{ + */ +/** @file + * @brief libexpr C bindings dealing with values + */ + +#include "nix_api_util.h" +#include "stdbool.h" +#include "stddef.h" +#include "stdint.h" + +#ifdef __cplusplus +extern "C" { +#endif +// cffi start + +// Type definitions +typedef enum { + NIX_TYPE_THUNK, + NIX_TYPE_INT, + NIX_TYPE_FLOAT, + NIX_TYPE_BOOL, + NIX_TYPE_STRING, + NIX_TYPE_PATH, + NIX_TYPE_NULL, + NIX_TYPE_ATTRS, + NIX_TYPE_LIST, + NIX_TYPE_FUNCTION, + NIX_TYPE_EXTERNAL +} ValueType; + +// forward declarations +typedef void Value; +typedef struct EvalState EvalState; +// type defs +/** @brief Stores an under-construction set of bindings + * @ingroup value_manip + * + * Do not reuse. + * @see nix_make_bindings_builder, nix_bindings_builder_free, nix_make_attrs + * @see nix_bindings_builder_insert + */ +typedef struct BindingsBuilder BindingsBuilder; + +/** @brief Stores an under-construction list + * @ingroup value_manip + * + * Do not reuse. + * @see nix_make_list_builder, nix_list_builder_free, nix_make_list + * @see nix_list_builder_insert + */ +typedef struct ListBuilder ListBuilder; + +/** @brief PrimOp function + * @ingroup primops + * + * Owned by the GC + * @see nix_alloc_primop, nix_init_primop + */ +typedef struct PrimOp PrimOp; +/** @brief External Value + * @ingroup Externals + * + * Owned by the GC + */ +typedef struct ExternalValue ExternalValue; + +/** @defgroup primops + * @brief Create your own primops + * @{ + */ +/** @brief Function pointer for primops + * When you want to return an error, call nix_set_err_msg(context, NIX_ERR_UNKNOWN, "your error message here"). + * + * @param[in] user_data Arbitrary data that was initially supplied to nix_alloc_primop + * @param[out] context Stores error information. + * @param[in] state Evaluator state + * @param[in] args list of arguments. Note that these can be thunks and should be forced using nix_value_force before + * use. + * @param[out] ret return value + * @see nix_alloc_primop, nix_init_primop + */ +typedef void (*PrimOpFun)(void * user_data, nix_c_context * context, EvalState * state, Value ** args, Value * ret); + +/** @brief Allocate a PrimOp + * + * Owned by the garbage collector. + * Use nix_gc_decref() when you're done with the returned PrimOp. + * + * @param[out] context Optional, stores error information + * @param[in] fun callback + * @param[in] arity expected number of function arguments + * @param[in] name function name + * @param[in] args array of argument names, NULL-terminated + * @param[in] doc optional, documentation for this primop + * @param[in] user_data optional, arbitrary data, passed to the callback when it's called + * @return primop, or null in case of errors + * @see nix_init_primop + */ +PrimOp * nix_alloc_primop( + nix_c_context * context, + PrimOpFun fun, + int arity, + const char * name, + const char ** args, + const char * doc, + void * user_data); + +/** @brief add a primop to the `builtins` attribute set + * + * Only applies to States created after this call. + * + * Moves your PrimOp content into the global evaluator + * registry, meaning your input PrimOp pointer is no longer usable. + * You are free to remove your references to it, + * after which it will be garbage collected. + * + * @param[out] context Optional, stores error information + * @return primop, or null in case of errors + * + */ +nix_err nix_register_primop(nix_c_context * context, PrimOp * primOp); +/** @} */ + +// Function prototypes + +/** @brief Allocate a Nix value + * + * Owned by the GC. Use nix_gc_decref() when you're done with the pointer + * @param[out] context Optional, stores error information + * @param[in] state nix evaluator state + * @return value, or null in case of errors + * + */ +Value * nix_alloc_value(nix_c_context * context, EvalState * state); + +/** @addtogroup value_manip Manipulating values + * @brief Functions to inspect and change Nix language values, represented by Value. + * @{ + */ +/** @name Getters + */ +/**@{*/ +/** @brief Get value type + * @param[out] context Optional, stores error information + * @param[in] value Nix value to inspect + * @return type of nix value + */ +ValueType nix_get_type(nix_c_context * context, const Value * value); + +/** @brief Get type name of value as defined in the evaluator + * @param[out] context Optional, stores error information + * @param[in] value Nix value to inspect + * @return type name, owned string + * @todo way to free the result + */ +const char * nix_get_typename(nix_c_context * context, const Value * value); + +/** @brief Get boolean value + * @param[out] context Optional, stores error information + * @param[in] value Nix value to inspect + * @return true or false, error info via context + */ +bool nix_get_bool(nix_c_context * context, const Value * value); + +/** @brief Get string + * @param[out] context Optional, stores error information + * @param[in] value Nix value to inspect + * @return string + * @return NULL in case of error. + */ +const char * nix_get_string(nix_c_context * context, const Value * value); + +/** @brief Get path as string + * @param[out] context Optional, stores error information + * @param[in] value Nix value to inspect + * @return string + * @return NULL in case of error. + */ +const char * nix_get_path_string(nix_c_context * context, const Value * value); + +/** @brief Get the length of a list + * @param[out] context Optional, stores error information + * @param[in] value Nix value to inspect + * @return length of list, error info via context + */ +unsigned int nix_get_list_size(nix_c_context * context, const Value * value); + +/** @brief Get the element count of an attrset + * @param[out] context Optional, stores error information + * @param[in] value Nix value to inspect + * @return attrset element count, error info via context + */ +unsigned int nix_get_attrs_size(nix_c_context * context, const Value * value); + +/** @brief Get float value in 64 bits + * @param[out] context Optional, stores error information + * @param[in] value Nix value to inspect + * @return float contents, error info via context + */ +double nix_get_float(nix_c_context * context, const Value * value); + +/** @brief Get int value + * @param[out] context Optional, stores error information + * @param[in] value Nix value to inspect + * @return int contents, error info via context + */ +int64_t nix_get_int(nix_c_context * context, const Value * value); + +/** @brief Get external reference + * @param[out] context Optional, stores error information + * @param[in] value Nix value to inspect + * @return reference to external, NULL in case of error + */ +ExternalValue * nix_get_external(nix_c_context * context, Value *); + +/** @brief Get the ix'th element of a list + * + * Owned by the GC. Use nix_gc_decref when you're done with the pointer + * @param[out] context Optional, stores error information + * @param[in] value Nix value to inspect + * @param[in] state nix evaluator state + * @param[in] ix list element to get + * @return value, NULL in case of errors + */ +Value * nix_get_list_byidx(nix_c_context * context, const Value * value, EvalState * state, unsigned int ix); + +/** @brief Get an attr by name + * + * Owned by the GC. Use nix_gc_decref when you're done with the pointer + * @param[out] context Optional, stores error information + * @param[in] value Nix value to inspect + * @param[in] state nix evaluator state + * @param[in] name attribute name + * @return value, NULL in case of errors + */ +Value * nix_get_attr_byname(nix_c_context * context, const Value * value, EvalState * state, const char * name); + +/** @brief Check if an attribute name exists on a value + * @param[out] context Optional, stores error information + * @param[in] value Nix value to inspect + * @param[in] state nix evaluator state + * @param[in] name attribute name + * @return value, error info via context + */ +bool nix_has_attr_byname(nix_c_context * context, const Value * value, EvalState * state, const char * name); + +/** @brief Get an attribute by index in the sorted bindings + * + * Also gives you the name. + * + * Owned by the GC. Use nix_gc_decref when you're done with the pointer + * @param[out] context Optional, stores error information + * @param[in] value Nix value to inspect + * @param[in] state nix evaluator state + * @param[in] i attribute index + * @param[out] name will store a pointer to the attribute name + * @return value, NULL in case of errors + */ +Value * +nix_get_attr_byidx(nix_c_context * context, const Value * value, EvalState * state, unsigned int i, const char ** name); + +/** @brief Get an attribute name by index in the sorted bindings + * + * Useful when you want the name but want to avoid evaluation. + * + * Owned by the nix EvalState + * @param[out] context Optional, stores error information + * @param[in] value Nix value to inspect + * @param[in] state nix evaluator state + * @param[in] i attribute index + * @return name, NULL in case of errors + */ +const char * nix_get_attr_name_byidx(nix_c_context * context, const Value * value, EvalState * state, unsigned int i); + +/**@}*/ +/** @name Initializers + * + * Values are typically "returned" by initializing already allocated memory that serves as the return value. + * For this reason, the construction of values is not tied their allocation. + * Nix is a language with immutable values. Respect this property by only initializing Values once; and only initialize + * Values that are meant to be initialized by you. Failing to adhere to these rules may lead to undefined behavior. + */ +/**@{*/ +/** @brief Set boolean value + * @param[out] context Optional, stores error information + * @param[out] value Nix value to modify + * @param[in] b the boolean value + * @return error code, NIX_OK on success. + */ +nix_err nix_init_bool(nix_c_context * context, Value * value, bool b); + +/** @brief Set a string + * @param[out] context Optional, stores error information + * @param[out] value Nix value to modify + * @param[in] str the string, copied + * @return error code, NIX_OK on success. + */ +nix_err nix_init_string(nix_c_context * context, Value * value, const char * str); + +/** @brief Set a path + * @param[out] context Optional, stores error information + * @param[out] value Nix value to modify + * @param[in] str the path string, copied + * @return error code, NIX_OK on success. + */ +nix_err nix_init_path_string(nix_c_context * context, EvalState * s, Value * value, const char * str); + +/** @brief Set a float + * @param[out] context Optional, stores error information + * @param[out] value Nix value to modify + * @param[in] d the float, 64-bits + * @return error code, NIX_OK on success. + */ +nix_err nix_init_float(nix_c_context * context, Value * value, double d); + +/** @brief Set an int + * @param[out] context Optional, stores error information + * @param[out] value Nix value to modify + * @param[in] i the int + * @return error code, NIX_OK on success. + */ + +nix_err nix_init_int(nix_c_context * context, Value * value, int64_t i); +/** @brief Set null + * @param[out] context Optional, stores error information + * @param[out] value Nix value to modify + * @return error code, NIX_OK on success. + */ + +nix_err nix_init_null(nix_c_context * context, Value * value); +/** @brief Set an external value + * @param[out] context Optional, stores error information + * @param[out] value Nix value to modify + * @param[in] val the external value to set. Will be GC-referenced by the value. + * @return error code, NIX_OK on success. + */ +nix_err nix_init_external(nix_c_context * context, Value * value, ExternalValue * val); + +/** @brief Create a list from a list builder + * @param[out] context Optional, stores error information + * @param[in] list_builder list builder to use. Make sure to unref this afterwards. + * @param[out] value Nix value to modify + * @return error code, NIX_OK on success. + */ +nix_err nix_make_list(nix_c_context * context, ListBuilder * list_builder, Value * value); + +/** @brief Create a list builder + * @param[out] context Optional, stores error information + * @param[in] state nix evaluator state + * @param[in] capacity how many bindings you'll add. Don't exceed. + * @return owned reference to a list builder. Make sure to unref when you're done. + */ +ListBuilder * nix_make_list_builder(nix_c_context * context, EvalState * state, size_t capacity); + +/** @brief Insert bindings into a builder + * @param[out] context Optional, stores error information + * @param[in] list_builder ListBuilder to insert into + * @param[in] index index to manipulate + * @param[in] value value to insert + * @return error code, NIX_OK on success. + */ +nix_err nix_list_builder_insert(nix_c_context * context, ListBuilder * list_builder, unsigned int index, Value * value); + +/** @brief Free a list builder + * + * Does not fail. + * @param[in] builder the builder to free + */ +void nix_list_builder_free(ListBuilder * list_builder); + +/** @brief Create an attribute set from a bindings builder + * @param[out] context Optional, stores error information + * @param[out] value Nix value to modify + * @param[in] b bindings builder to use. Make sure to unref this afterwards. + * @return error code, NIX_OK on success. + */ +nix_err nix_make_attrs(nix_c_context * context, Value * value, BindingsBuilder * b); + +/** @brief Set primop + * @param[out] context Optional, stores error information + * @param[out] value Nix value to modify + * @param[in] op primop, will be gc-referenced by the value + * @see nix_alloc_primop + * @return error code, NIX_OK on success. + */ +nix_err nix_init_primop(nix_c_context * context, Value * value, PrimOp * op); +/** @brief Copy from another value + * @param[out] context Optional, stores error information + * @param[out] value Nix value to modify + * @param[in] source value to copy from + * @return error code, NIX_OK on success. + */ +nix_err nix_copy_value(nix_c_context * context, Value * value, Value * source); +/**@}*/ + +/** @brief Create a bindings builder +* @param[out] context Optional, stores error information +* @param[in] state nix evaluator state +* @param[in] capacity how many bindings you'll add. Don't exceed. +* @return owned reference to a bindings builder. Make sure to unref when you're +done. +*/ +BindingsBuilder * nix_make_bindings_builder(nix_c_context * context, EvalState * state, size_t capacity); + +/** @brief Insert bindings into a builder + * @param[out] context Optional, stores error information + * @param[in] builder BindingsBuilder to insert into + * @param[in] name attribute name, copied into the symbol store + * @param[in] value value to give the binding + * @return error code, NIX_OK on success. + */ +nix_err +nix_bindings_builder_insert(nix_c_context * context, BindingsBuilder * builder, const char * name, Value * value); + +/** @brief Free a bindings builder + * + * Does not fail. + * @param[in] builder the builder to free + */ +void nix_bindings_builder_free(BindingsBuilder * builder); +/**@}*/ + +// cffi end +#ifdef __cplusplus +} +#endif + +/** @} */ +#endif // NIX_API_VALUE_H diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index 5e2f71649..648032ea3 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -897,7 +897,6 @@ void Value::mkStringMove(const char * s, const NixStringContext & context) copyContextToValue(*this, context); } - void Value::mkPath(const SourcePath & path) { mkPath(&*path.accessor, makeImmutableString(path.path.abs())); @@ -1665,7 +1664,8 @@ void EvalState::callFunction(Value & fun, size_t nrArgs, Value * * args, Value & try { fn->fun(*this, vCur.determinePos(noPos), args, vCur); } catch (Error & e) { - addErrorTrace(e, pos, "while calling the '%1%' builtin", fn->name); + if (fn->addTrace) + addErrorTrace(e, pos, "while calling the '%1%' builtin", fn->name); throw; } @@ -1713,7 +1713,8 @@ void EvalState::callFunction(Value & fun, size_t nrArgs, Value * * args, Value & // so the debugger allows to inspect the wrong parameters passed to the builtin. fn->fun(*this, vCur.determinePos(noPos), vArgs, vCur); } catch (Error & e) { - addErrorTrace(e, pos, "while calling the '%1%' builtin", fn->name); + if (fn->addTrace) + addErrorTrace(e, pos, "while calling the '%1%' builtin", fn->name); throw; } diff --git a/src/libexpr/eval.hh b/src/libexpr/eval.hh index f15d19653..4d53bcde6 100644 --- a/src/libexpr/eval.hh +++ b/src/libexpr/eval.hh @@ -17,6 +17,7 @@ #include #include #include +#include namespace nix { @@ -69,10 +70,17 @@ struct PrimOp */ const char * doc = nullptr; + /** + * Add a trace item, `while calling the '' builtin` + * + * This is used to remove the redundant item for `builtins.addErrorContext`. + */ + bool addTrace = true; + /** * Implementation of the primop. */ - PrimOpFun fun; + std::function::type> fun; /** * Optional experimental for this to be gated on. diff --git a/src/libexpr/flake/flake.cc b/src/libexpr/flake/flake.cc index 5ce1c85cb..b329f2961 100644 --- a/src/libexpr/flake/flake.cc +++ b/src/libexpr/flake/flake.cc @@ -10,6 +10,7 @@ #include "finally.hh" #include "fetch-settings.hh" #include "value-to-json.hh" +#include "local-fs-store.hh" namespace nix { @@ -754,7 +755,17 @@ void callFlake(EvalState & state, auto lockedNode = node.dynamic_pointer_cast(); - auto [storePath, subdir] = state.store->toStorePath(sourcePath.path.abs()); + // FIXME: This is a hack to support chroot stores. Remove this + // once we can pass a sourcePath rather than a storePath to + // call-flake.nix. + auto path = sourcePath.path.abs(); + if (auto store = state.store.dynamic_pointer_cast()) { + auto realStoreDir = store->getRealStoreDir(); + if (isInDir(path, realStoreDir)) + path = store->storeDir + path.substr(realStoreDir.size()); + } + + auto [storePath, subdir] = state.store->toStorePath(path); emitTreeAttrs( state, diff --git a/src/libexpr/local.mk b/src/libexpr/local.mk index 0c3e36750..ecadc5e5d 100644 --- a/src/libexpr/local.mk +++ b/src/libexpr/local.mk @@ -11,8 +11,11 @@ libexpr_SOURCES := \ $(wildcard $(d)/flake/*.cc) \ $(d)/lexer-tab.cc \ $(d)/parser-tab.cc +# Not just for this library itself, but also for downstream libraries using this library -libexpr_CXXFLAGS += -I src/libutil -I src/libstore -I src/libfetchers -I src/libmain -I src/libexpr +INCLUDE_libexpr := -I $(d) + +libexpr_CXXFLAGS += $(INCLUDE_libutil) $(INCLUDE_libstore) $(INCLUDE_libfetchers) $(INCLUDE_libmain) $(INCLUDE_libexpr) libexpr_LIBS = libutil libstore libfetchers diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index 2d93272cd..aa472be7f 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -826,7 +826,7 @@ static void prim_addErrorContext(EvalState & state, const PosIdx pos, Value * * auto message = state.coerceToString(pos, *args[0], context, "while evaluating the error message passed to builtins.addErrorContext", false, false).toOwned(); - e.addTrace(nullptr, HintFmt(message)); + e.addTrace(nullptr, HintFmt(message), TracePrint::Always); throw; } } @@ -834,6 +834,8 @@ static void prim_addErrorContext(EvalState & state, const PosIdx pos, Value * * static RegisterPrimOp primop_addErrorContext(PrimOp { .name = "__addErrorContext", .arity = 2, + // The normal trace item is redundant + .addTrace = false, .fun = prim_addErrorContext, }); @@ -1125,7 +1127,7 @@ drvName, Bindings * attrs, Value & v) bool contentAddressed = false; bool isImpure = false; std::optional outputHash; - std::string outputHashAlgo; + std::optional outputHashAlgo; std::optional ingestionMethod; StringSet outputs; @@ -1224,7 +1226,7 @@ drvName, Bindings * attrs, Value & v) else if (i->name == state.sOutputHash) outputHash = state.forceStringNoCtx(*i->value, pos, context_below); else if (i->name == state.sOutputHashAlgo) - outputHashAlgo = state.forceStringNoCtx(*i->value, pos, context_below); + outputHashAlgo = parseHashAlgoOpt(state.forceStringNoCtx(*i->value, pos, context_below)); else if (i->name == state.sOutputHashMode) handleHashMode(state.forceStringNoCtx(*i->value, pos, context_below)); else if (i->name == state.sOutputs) { @@ -1242,7 +1244,7 @@ drvName, Bindings * attrs, Value & v) if (i->name == state.sBuilder) drv.builder = std::move(s); else if (i->name == state.sSystem) drv.platform = std::move(s); else if (i->name == state.sOutputHash) outputHash = std::move(s); - else if (i->name == state.sOutputHashAlgo) outputHashAlgo = std::move(s); + else if (i->name == state.sOutputHashAlgo) outputHashAlgo = parseHashAlgoOpt(s); else if (i->name == state.sOutputHashMode) handleHashMode(s); else if (i->name == state.sOutputs) handleOutputs(tokenizeString(s)); @@ -1325,7 +1327,7 @@ drvName, Bindings * attrs, Value & v) "multiple outputs are not supported in fixed-output derivations" ).atPos(v).debugThrow(); - auto h = newHashAllowEmpty(*outputHash, parseHashAlgoOpt(outputHashAlgo)); + auto h = newHashAllowEmpty(*outputHash, outputHashAlgo); auto method = ingestionMethod.value_or(FileIngestionMethod::Flat); @@ -1345,7 +1347,7 @@ drvName, Bindings * attrs, Value & v) state.error("derivation cannot be both content-addressed and impure") .atPos(v).debugThrow(); - auto ha = parseHashAlgoOpt(outputHashAlgo).value_or(HashAlgorithm::SHA256); + auto ha = outputHashAlgo.value_or(HashAlgorithm::SHA256); auto method = ingestionMethod.value_or(FileIngestionMethod::Recursive); for (auto & i : outputs) { @@ -1568,23 +1570,50 @@ static RegisterPrimOp primop_pathExists({ .fun = prim_pathExists, }); +// Ideally, all trailing slashes should have been removed, but it's been like this for +// almost a decade as of writing. Changing it will affect reproducibility. +static std::string_view legacyBaseNameOf(std::string_view path) +{ + if (path.empty()) + return ""; + + auto last = path.size() - 1; + if (path[last] == '/' && last > 0) + last -= 1; + + auto pos = path.rfind('/', last); + if (pos == path.npos) + pos = 0; + else + pos += 1; + + return path.substr(pos, last - pos + 1); +} + /* Return the base name of the given string, i.e., everything following the last slash. */ static void prim_baseNameOf(EvalState & state, const PosIdx pos, Value * * args, Value & v) { NixStringContext context; - v.mkString(baseNameOf(*state.coerceToString(pos, *args[0], context, + v.mkString(legacyBaseNameOf(*state.coerceToString(pos, *args[0], context, "while evaluating the first argument passed to builtins.baseNameOf", false, false)), context); } static RegisterPrimOp primop_baseNameOf({ .name = "baseNameOf", - .args = {"s"}, + .args = {"x"}, .doc = R"( - Return the *base name* of the string *s*, that is, everything - following the final slash in the string. This is similar to the GNU - `basename` command. + Return the *base name* of either a [path value](@docroot@/language/values.md#type-path) *x* or a string *x*, depending on which type is passed, and according to the following rules. + + For a path value, the *base name* is considered to be the part of the path after the last directory separator, including any file extensions. + This is the simple case, as path values don't have trailing slashes. + + When the argument is a string, a more involved logic applies. If the string ends with a `/`, only this one final slash is removed. + + After this, the *base name* is returned as previously described, assuming `/` as the directory separator. (Note that evaluation must be platform independent.) + + This is somewhat similar to the [GNU `basename`](https://www.gnu.org/software/coreutils/manual/html_node/basename-invocation.html) command, but GNU `basename` will strip any number of trailing slashes. )", .fun = prim_baseNameOf, }); @@ -3375,8 +3404,11 @@ static void prim_sort(EvalState & state, const PosIdx pos, Value * * args, Value callFunction. */ /* TODO: (layus) this is absurd. An optimisation like this should be outside the lambda creation */ - if (args[0]->isPrimOp() && args[0]->primOp->fun == prim_lessThan) - return CompareValues(state, noPos, "while evaluating the ordering function passed to builtins.sort")(a, b); + if (args[0]->isPrimOp()) { + auto ptr = args[0]->primOp->fun.target(); + if (ptr && *ptr == prim_lessThan) + return CompareValues(state, noPos, "while evaluating the ordering function passed to builtins.sort")(a, b); + } Value * vs[] = {a, b}; Value vBool; @@ -3832,7 +3864,7 @@ static RegisterPrimOp primop_stringLength({ .name = "__stringLength", .args = {"e"}, .doc = R"( - Return the length of the string *e*. If *e* is not a string, + Return the number of bytes of the string *e*. If *e* is not a string, evaluation is aborted. )", .fun = prim_stringLength, diff --git a/src/libexpr/search-path.cc b/src/libexpr/search-path.cc index a25767496..e2c3e050a 100644 --- a/src/libexpr/search-path.cc +++ b/src/libexpr/search-path.cc @@ -44,7 +44,7 @@ SearchPath::Elem SearchPath::Elem::parse(std::string_view rawElem) } -SearchPath parseSearchPath(const Strings & rawElems) +SearchPath SearchPath::parse(const Strings & rawElems) { SearchPath res; for (auto & rawElem : rawElems) diff --git a/src/libexpr/value.hh b/src/libexpr/value.hh index 335801b34..b7b3c6434 100644 --- a/src/libexpr/value.hh +++ b/src/libexpr/value.hh @@ -328,6 +328,7 @@ public: } void mkPath(const SourcePath & path); + void mkPath(std::string_view path); inline void mkPath(InputAccessor * accessor, const char * path) { diff --git a/src/libfetchers/fetch-settings.hh b/src/libfetchers/fetch-settings.hh index 705ee16e6..9e87d0725 100644 --- a/src/libfetchers/fetch-settings.hh +++ b/src/libfetchers/fetch-settings.hh @@ -78,7 +78,6 @@ struct FetchSettings : public Config )", {}, true, Xp::Flakes}; - Setting useRegistries{this, true, "use-registries", "Whether to use flake registries to resolve flake references.", {}, true, Xp::Flakes}; @@ -98,6 +97,22 @@ struct FetchSettings : public Config empty, the summary is generated based on the action performed. )", {}, true, Xp::Flakes}; + + Setting trustTarballsFromGitForges{ + this, true, "trust-tarballs-from-git-forges", + R"( + If enabled (the default), Nix will consider tarballs from + GitHub and similar Git forges to be locked if a Git revision + is specified, + e.g. `github:NixOS/patchelf/7c2f768bf9601268a4e71c2ebe91e2011918a70f`. + This requires Nix to trust that the provider will return the + correct contents for the specified Git revision. + + If disabled, such tarballs are only considered locked if a + `narHash` attribute is specified, + e.g. `github:NixOS/patchelf/7c2f768bf9601268a4e71c2ebe91e2011918a70f?narHash=sha256-PPXqKY2hJng4DBVE0I4xshv/vGLUskL7jl53roB8UdU%3D`. + )"}; + }; // FIXME: don't use a global variable. diff --git a/src/libfetchers/git-utils.cc b/src/libfetchers/git-utils.cc index b723554cc..5e560f5f3 100644 --- a/src/libfetchers/git-utils.cc +++ b/src/libfetchers/git-utils.cc @@ -348,7 +348,7 @@ struct GitRepoImpl : GitRepo, std::enable_shared_from_this { auto act = (Activity *) payload; act->result(resFetchStatus, trim(std::string_view(str, len))); - return _isInterrupted ? -1 : 0; + return getInterrupted() ? -1 : 0; } static int transferProgressCallback(const git_indexer_progress * stats, void * payload) @@ -361,7 +361,7 @@ struct GitRepoImpl : GitRepo, std::enable_shared_from_this stats->indexed_deltas, stats->total_deltas, stats->received_bytes / (1024.0 * 1024.0))); - return _isInterrupted ? -1 : 0; + return getInterrupted() ? -1 : 0; } void fetch( diff --git a/src/libfetchers/github.cc b/src/libfetchers/github.cc index 8100afe4d..60e323464 100644 --- a/src/libfetchers/github.cc +++ b/src/libfetchers/github.cc @@ -294,7 +294,9 @@ struct GitArchiveInputScheme : InputScheme Git revision alone, we also require a NAR hash for locking. FIXME: in the future, we may want to require a Git tree hash instead of a NAR hash. */ - return input.getRev().has_value() && input.getNarHash().has_value(); + return input.getRev().has_value() + && (fetchSettings.trustTarballsFromGitForges || + input.getNarHash().has_value()); } std::optional experimentalFeature() const override diff --git a/src/libfetchers/local.mk b/src/libfetchers/local.mk index e54db4937..0fef1466b 100644 --- a/src/libfetchers/local.mk +++ b/src/libfetchers/local.mk @@ -5,8 +5,18 @@ libfetchers_NAME = libnixfetchers libfetchers_DIR := $(d) libfetchers_SOURCES := $(wildcard $(d)/*.cc) +ifdef HOST_UNIX + libfetchers_SOURCES += $(wildcard $(d)/unix/*.cc) +endif -libfetchers_CXXFLAGS += -I src/libutil -I src/libstore +# Not just for this library itself, but also for downstream libraries using this library + +INCLUDE_libfetchers := -I $(d) +ifdef HOST_UNIX + INCLUDE_libfetchers += -I $(d)/unix +endif + +libfetchers_CXXFLAGS += $(INCLUDE_libutil) $(INCLUDE_libstore) $(INCLUDE_libfetchers) libfetchers_LDFLAGS += $(THREAD_LDFLAGS) $(LIBGIT2_LIBS) -larchive diff --git a/src/libfetchers/git.cc b/src/libfetchers/unix/git.cc similarity index 99% rename from src/libfetchers/git.cc rename to src/libfetchers/unix/git.cc index 587d06627..e852a28a0 100644 --- a/src/libfetchers/git.cc +++ b/src/libfetchers/unix/git.cc @@ -147,9 +147,12 @@ std::vector getPublicKeys(const Attrs & attrs) { std::vector publicKeys; if (attrs.contains("publicKeys")) { - nlohmann::json publicKeysJson = nlohmann::json::parse(getStrAttr(attrs, "publicKeys")); - ensureType(publicKeysJson, nlohmann::json::value_t::array); - publicKeys = publicKeysJson.get>(); + auto pubKeysJson = nlohmann::json::parse(getStrAttr(attrs, "publicKeys")); + auto & pubKeys = getArray(pubKeysJson); + + for (auto & key : pubKeys) { + publicKeys.push_back(key); + } } if (attrs.contains("publicKey")) publicKeys.push_back(PublicKey{maybeGetStrAttr(attrs, "keytype").value_or("ssh-ed25519"),getStrAttr(attrs, "publicKey")}); diff --git a/src/libfetchers/mercurial.cc b/src/libfetchers/unix/mercurial.cc similarity index 100% rename from src/libfetchers/mercurial.cc rename to src/libfetchers/unix/mercurial.cc diff --git a/src/libmain/local.mk b/src/libmain/local.mk index 5c7061863..d41c49dd7 100644 --- a/src/libmain/local.mk +++ b/src/libmain/local.mk @@ -5,8 +5,13 @@ libmain_NAME = libnixmain libmain_DIR := $(d) libmain_SOURCES := $(wildcard $(d)/*.cc) +ifdef HOST_UNIX + libmain_SOURCES += $(wildcard $(d)/unix/*.cc) +endif -libmain_CXXFLAGS += -I src/libutil -I src/libstore +INCLUDE_libmain := -I $(d) + +libmain_CXXFLAGS += $(INCLUDE_libutil) $(INCLUDE_libstore) $(INCLUDE_libmain) libmain_LDFLAGS += $(OPENSSL_LIBS) diff --git a/src/libmain/progress-bar.cc b/src/libmain/progress-bar.cc index 3aa012ee1..ce45eae2b 100644 --- a/src/libmain/progress-bar.cc +++ b/src/libmain/progress-bar.cc @@ -123,14 +123,18 @@ public: } void pause() override { - state_.lock()->paused = true; - writeToStderr("\r\e[K"); + auto state (state_.lock()); + state->paused = true; + if (state->active) + writeToStderr("\r\e[K"); } void resume() override { - state_.lock()->paused = false; - writeToStderr("\r\e[K"); - state_.lock()->haveUpdate = true; + auto state (state_.lock()); + state->paused = false; + if (state->active) + writeToStderr("\r\e[K"); + state->haveUpdate = true; updateCV.notify_one(); } @@ -162,9 +166,7 @@ public: writeToStderr("\r\e[K" + filterANSIEscapes(s, !isTTY) + ANSI_NORMAL "\n"); draw(state); } else { - auto s2 = s + ANSI_NORMAL "\n"; - if (!isTTY) s2 = filterANSIEscapes(s2, true); - writeToStderr(s2); + writeToStderr(filterANSIEscapes(s, !isTTY) + "\n"); } } @@ -519,7 +521,7 @@ public: std::optional ask(std::string_view msg) override { auto state(state_.lock()); - if (!state->active || !isatty(STDIN_FILENO)) return {}; + if (!state->active) return {}; std::cerr << fmt("\r\e[K%s ", msg); auto s = trim(readLine(STDIN_FILENO)); if (s.size() != 1) return {}; @@ -535,7 +537,7 @@ public: Logger * makeProgressBar() { - return new ProgressBar(shouldANSI()); + return new ProgressBar(isTTY()); } void startProgressBar() diff --git a/src/libmain/shared.cc b/src/libmain/shared.cc index e9dd82f92..e0ba1f9ce 100644 --- a/src/libmain/shared.cc +++ b/src/libmain/shared.cc @@ -121,7 +121,7 @@ void initNix() initLibStore(); - startSignalHandlerThread(); + unix::startSignalHandlerThread(); /* Reset SIGCHLD to its default. */ struct sigaction act; @@ -311,7 +311,7 @@ void printVersion(const std::string & programName) void showManPage(const std::string & name) { restoreProcessContext(); - setenv("MANPATH", settings.nixManDir.c_str(), 1); + setEnv("MANPATH", settings.nixManDir.c_str()); execlp("man", "man", name.c_str(), nullptr); throw SysError("command 'man %1%' failed", name.c_str()); } @@ -372,7 +372,7 @@ RunPager::RunPager() if (dup2(toPager.readSide.get(), STDIN_FILENO) == -1) throw SysError("dupping stdin"); if (!getenv("LESS")) - setenv("LESS", "FRSXMK", 1); + setEnv("LESS", "FRSXMK"); restoreProcessContext(); if (pager) execl("/bin/sh", "sh", "-c", pager, nullptr); diff --git a/src/libstore-c/local.mk b/src/libstore-c/local.mk new file mode 100644 index 000000000..5e3eff06a --- /dev/null +++ b/src/libstore-c/local.mk @@ -0,0 +1,21 @@ +libraries += libstorec + +libstorec_NAME = libnixstorec + +libstorec_DIR := $(d) + +libstorec_SOURCES := $(wildcard $(d)/*.cc) + +libstorec_LIBS = libutil libstore libutilc + +libstorec_LDFLAGS += $(THREAD_LDFLAGS) + +# Not just for this library itself, but also for downstream libraries using this library + +INCLUDE_libstorec := -I $(d) +libstorec_CXXFLAGS += $(INCLUDE_libutil) $(INCLUDE_libutilc) \ + $(INCLUDE_libstore) $(INCLUDE_libstorec) + +$(eval $(call install-file-in, $(d)/nix-store-c.pc, $(libdir)/pkgconfig, 0644)) + +libstorec_FORCE_INSTALL := 1 diff --git a/src/libstore-c/nix-store-c.pc.in b/src/libstore-c/nix-store-c.pc.in new file mode 100644 index 000000000..de3c7b4c6 --- /dev/null +++ b/src/libstore-c/nix-store-c.pc.in @@ -0,0 +1,9 @@ +prefix=@prefix@ +libdir=@libdir@ +includedir=@includedir@ + +Name: Nix +Description: Nix Store - C API +Version: @PACKAGE_VERSION@ +Libs: -L${libdir} -lnixstorec -lnixutilc +Cflags: -I${includedir}/nix diff --git a/src/libstore-c/nix_api_store.cc b/src/libstore-c/nix_api_store.cc new file mode 100644 index 000000000..d80ba332e --- /dev/null +++ b/src/libstore-c/nix_api_store.cc @@ -0,0 +1,134 @@ +#include "nix_api_store.h" +#include "nix_api_store_internal.h" +#include "nix_api_util.h" +#include "nix_api_util_internal.h" + +#include "path.hh" +#include "store-api.hh" +#include "build-result.hh" + +#include "globals.hh" + +nix_err nix_libstore_init(nix_c_context * context) +{ + if (context) + context->last_err_code = NIX_OK; + try { + nix::initLibStore(); + } + NIXC_CATCH_ERRS +} + +nix_err nix_init_plugins(nix_c_context * context) +{ + if (context) + context->last_err_code = NIX_OK; + try { + nix::initPlugins(); + } + NIXC_CATCH_ERRS +} + +Store * nix_store_open(nix_c_context * context, const char * uri, const char *** params) +{ + if (context) + context->last_err_code = NIX_OK; + try { + std::string uri_str = uri ? uri : ""; + + if (uri_str.empty()) + return new Store{nix::openStore()}; + + if (!params) + return new Store{nix::openStore(uri_str)}; + + nix::Store::Params params_map; + for (size_t i = 0; params[i] != nullptr; i++) { + params_map[params[i][0]] = params[i][1]; + } + return new Store{nix::openStore(uri_str, params_map)}; + } + NIXC_CATCH_ERRS_NULL +} + +void nix_store_free(Store * store) +{ + delete store; +} + +nix_err nix_store_get_uri(nix_c_context * context, Store * store, void * callback, void * user_data) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto res = store->ptr->getUri(); + return call_nix_get_string_callback(res, callback, user_data); + } + NIXC_CATCH_ERRS +} + +nix_err nix_store_get_version(nix_c_context * context, Store * store, void * callback, void * user_data) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto res = store->ptr->getVersion(); + return call_nix_get_string_callback(res.value_or(""), callback, user_data); + } + NIXC_CATCH_ERRS +} + +bool nix_store_is_valid_path(nix_c_context * context, Store * store, StorePath * path) +{ + if (context) + context->last_err_code = NIX_OK; + try { + return store->ptr->isValidPath(path->path); + } + NIXC_CATCH_ERRS_RES(false); +} + +StorePath * nix_store_parse_path(nix_c_context * context, Store * store, const char * path) +{ + if (context) + context->last_err_code = NIX_OK; + try { + nix::StorePath s = store->ptr->parseStorePath(path); + return new StorePath{std::move(s)}; + } + NIXC_CATCH_ERRS_NULL +} + +nix_err nix_store_realise( + nix_c_context * context, + Store * store, + StorePath * path, + void * userdata, + void (*callback)(void * userdata, const char *, const char *)) +{ + if (context) + context->last_err_code = NIX_OK; + try { + + const std::vector paths{nix::DerivedPath::Built{ + .drvPath = nix::makeConstantStorePathRef(path->path), .outputs = nix::OutputsSpec::All{}}}; + + const auto nixStore = store->ptr; + auto results = nixStore->buildPathsWithResults(paths, nix::bmNormal, nixStore); + + if (callback) { + for (const auto & result : results) { + for (const auto & [outputName, realisation] : result.builtOutputs) { + auto op = store->ptr->printStorePath(realisation.outPath); + callback(userdata, outputName.c_str(), op.c_str()); + } + } + } + } + NIXC_CATCH_ERRS +} + +void nix_store_path_free(StorePath * sp) +{ + delete sp; +} diff --git a/src/libstore-c/nix_api_store.h b/src/libstore-c/nix_api_store.h new file mode 100644 index 000000000..1309f99b7 --- /dev/null +++ b/src/libstore-c/nix_api_store.h @@ -0,0 +1,148 @@ +#ifndef NIX_API_STORE_H +#define NIX_API_STORE_H +/** + * @defgroup libstore libstore + * @brief C bindings for nix libstore + * + * libstore is used for talking to a Nix store + * @{ + */ +/** @file + * @brief Main entry for the libstore C bindings + */ + +#include "nix_api_util.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif +// cffi start + +/** @brief Reference to a Nix store */ +typedef struct Store Store; +/** @brief Nix store path */ +typedef struct StorePath StorePath; + +/** + * @brief Initializes the Nix store library + * + * This function should be called before creating a Store + * This function can be called multiple times. + * + * @param[out] context Optional, stores error information + * @return NIX_OK if the initialization was successful, an error code otherwise. + */ +nix_err nix_libstore_init(nix_c_context * context); + +/** + * @brief Loads the plugins specified in Nix's plugin-files setting. + * + * Call this once, after calling your desired init functions and setting + * relevant settings. + * + * @param[out] context Optional, stores error information + * @return NIX_OK if the initialization was successful, an error code otherwise. + */ +nix_err nix_init_plugins(nix_c_context * context); + +/** + * @brief Open a nix store + * Store instances may share state and resources behind the scenes. + * @param[out] context Optional, stores error information + * @param[in] uri URI of the nix store, copied + * @param[in] params optional, array of key-value pairs, {{"endpoint", + * "https://s3.local"}} + * @return a Store pointer, NULL in case of errors + * @see nix_store_free + */ +Store * nix_store_open(nix_c_context *, const char * uri, const char *** params); + +/** + * @brief Deallocate a nix store and free any resources if not also held by other Store instances. + * + * Does not fail. + * + * @param[in] store the store to free + */ +void nix_store_free(Store * store); + +/** + * @brief get the URI of a nix store + * @param[out] context Optional, stores error information + * @param[in] store nix store reference + * @param[in] callback Called with the URI. + * @param[in] user_data optional, arbitrary data, passed to the callback when it's called. + * @see nix_get_string_callback + * @return error code, NIX_OK on success. + */ +nix_err nix_store_get_uri(nix_c_context * context, Store * store, void * callback, void * user_data); + +// returns: owned StorePath* +/** + * @brief Parse a Nix store path into a StorePath + * + * @note Don't forget to free this path using nix_store_path_free()! + * @param[out] context Optional, stores error information + * @param[in] store nix store reference + * @param[in] path Path string to parse, copied + * @return owned store path, NULL on error + */ +StorePath * nix_store_parse_path(nix_c_context * context, Store * store, const char * path); + +/** @brief Deallocate a StorePath + * + * Does not fail. + * @param[in] p the path to free + */ +void nix_store_path_free(StorePath * p); + +/** + * @brief Check if a StorePath is valid (i.e. that corresponding store object and its closure of references exists in + * the store) + * @param[out] context Optional, stores error information + * @param[in] store Nix Store reference + * @param[in] path Path to check + * @return true or false, error info in context + */ +bool nix_store_is_valid_path(nix_c_context * context, Store * store, StorePath * path); +// nix_err nix_store_ensure(Store*, const char*); +// nix_err nix_store_build_paths(Store*); +/** + * @brief Realise a Nix store path + * + * Blocking, calls callback once for each realised output + * + * @param[out] context Optional, stores error information + * @param[in] store Nix Store reference + * @param[in] path Path to build + * @param[in] userdata data to pass to every callback invocation + * @param[in] callback called for every realised output + */ +nix_err nix_store_realise( + nix_c_context * context, + Store * store, + StorePath * path, + void * userdata, + void (*callback)(void * userdata, const char * outname, const char * out)); + +/** + * @brief get the version of a nix store. + * If the store doesn't have a version (like the dummy store), returns an empty string. + * @param[out] context Optional, stores error information + * @param[in] store nix store reference + * @param[in] callback Called with the version. + * @param[in] user_data optional, arbitrary data, passed to the callback when it's called. + * @see nix_get_string_callback + * @return error code, NIX_OK on success. + */ +nix_err nix_store_get_version(nix_c_context * context, Store * store, void * callback, void * user_data); + +// cffi end +#ifdef __cplusplus +} +#endif +/** + * @} + */ +#endif // NIX_API_STORE_H diff --git a/src/libstore-c/nix_api_store_internal.h b/src/libstore-c/nix_api_store_internal.h new file mode 100644 index 000000000..13db0c07c --- /dev/null +++ b/src/libstore-c/nix_api_store_internal.h @@ -0,0 +1,15 @@ +#ifndef NIX_API_STORE_INTERNAL_H +#define NIX_API_STORE_INTERNAL_H +#include "store-api.hh" + +struct Store +{ + nix::ref ptr; +}; + +struct StorePath +{ + nix::StorePath path; +}; + +#endif diff --git a/src/libstore/binary-cache-store.cc b/src/libstore/binary-cache-store.cc index bea2bb370..97b6ec052 100644 --- a/src/libstore/binary-cache-store.cc +++ b/src/libstore/binary-cache-store.cc @@ -124,14 +124,6 @@ void BinaryCacheStore::writeNarInfo(ref narInfo) diskCache->upsertNarInfo(getUri(), std::string(narInfo->path.hashPart()), std::shared_ptr(narInfo)); } -AutoCloseFD openFile(const Path & path) -{ - auto fd = open(path.c_str(), O_RDONLY | O_CLOEXEC); - if (!fd) - throw SysError("opening file '%1%'", path); - return fd; -} - ref BinaryCacheStore::addToStoreCommon( Source & narSource, RepairFlag repair, CheckSigsFlag checkSigs, std::function mkInfo) diff --git a/src/libstore/build/local-derivation-goal.cc b/src/libstore/build/local-derivation-goal.cc index 612434e4d..f8794e783 100644 --- a/src/libstore/build/local-derivation-goal.cc +++ b/src/libstore/build/local-derivation-goal.cc @@ -17,7 +17,6 @@ #include "cgroup.hh" #include "personality.hh" #include "current-process.hh" -#include "namespaces.hh" #include "child.hh" #include "unix-domain-socket.hh" #include "posix-fs-canonicalise.hh" @@ -40,18 +39,19 @@ /* Includes required for chroot support. */ #if __linux__ -#include -#include -#include -#include -#include -#include -#include -#include -#if HAVE_SECCOMP -#include -#endif -#define pivot_root(new_root, put_old) (syscall(SYS_pivot_root, new_root, put_old)) +# include +# include +# include +# include +# include +# include +# include +# include +# include "namespaces.hh" +# if HAVE_SECCOMP +# include +# endif +# define pivot_root(new_root, put_old) (syscall(SYS_pivot_root, new_root, put_old)) #endif #if __APPLE__ @@ -488,7 +488,7 @@ void LocalDerivationGoal::startBuilder() /* Create a temporary directory where the build will take place. */ - tmpDir = createTempDir("", "nix-build-" + std::string(drvPath.name()), false, false, 0700); + tmpDir = createTempDir(settings.buildDir.get().value_or(""), "nix-build-" + std::string(drvPath.name()), false, false, 0700); chownToBuilder(tmpDir); @@ -2053,13 +2053,13 @@ void LocalDerivationGoal::runChild() i.first, i.second.source); std::string path = i.first; - struct stat st; - if (lstat(path.c_str(), &st)) { - if (i.second.optional && errno == ENOENT) + auto optSt = maybeLstat(path.c_str()); + if (!optSt) { + if (i.second.optional) continue; - throw SysError("getting attributes of path '%s", path); + throw SysError("getting attributes of required path '%s", path); } - if (S_ISDIR(st.st_mode)) + if (S_ISDIR(optSt->st_mode)) sandboxProfile += fmt("\t(subpath \"%s\")\n", path); else sandboxProfile += fmt("\t(literal \"%s\")\n", path); @@ -2089,7 +2089,7 @@ void LocalDerivationGoal::runChild() bool allowLocalNetworking = parsedDrv->getBoolAttr("__darwinAllowLocalNetworking"); /* The tmpDir in scope points at the temporary build directory for our derivation. Some packages try different mechanisms - to find temporary directories, so we want to open up a broader place for them to dump their files, if needed. */ + to find temporary directories, so we want to open up a broader place for them to put their files, if needed. */ Path globalTmpDir = canonPath(defaultTempDir(), true); /* They don't like trailing slashes on subpath directives */ @@ -2271,14 +2271,12 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() continue; } - struct stat st; - if (lstat(actualPath.c_str(), &st) == -1) { - if (errno == ENOENT) - throw BuildError( - "builder for '%s' failed to produce output path for output '%s' at '%s'", - worker.store.printStorePath(drvPath), outputName, actualPath); - throw SysError("getting attributes of path '%s'", actualPath); - } + auto optSt = maybeLstat(actualPath.c_str()); + if (!optSt) + throw BuildError( + "builder for '%s' failed to produce output path for output '%s' at '%s'", + worker.store.printStorePath(drvPath), outputName, actualPath); + struct stat & st = *optSt; #ifndef __CYGWIN__ /* Check that the output is not group or world writable, as diff --git a/src/libstore/builtins/buildenv.cc b/src/libstore/builtins/buildenv.cc index 1ed7b39cc..31a6b32f1 100644 --- a/src/libstore/builtins/buildenv.cc +++ b/src/libstore/builtins/buildenv.cc @@ -64,9 +64,9 @@ static void createLinks(State & state, const Path & srcDir, const Path & dstDir, continue; else if (S_ISDIR(srcSt.st_mode)) { - struct stat dstSt; - auto res = lstat(dstFile.c_str(), &dstSt); - if (res == 0) { + auto dstStOpt = maybeLstat(dstFile.c_str()); + if (dstStOpt) { + auto & dstSt = *dstStOpt; if (S_ISDIR(dstSt.st_mode)) { createLinks(state, srcFile, dstFile, priority); continue; @@ -82,14 +82,13 @@ static void createLinks(State & state, const Path & srcDir, const Path & dstDir, createLinks(state, srcFile, dstFile, priority); continue; } - } else if (errno != ENOENT) - throw SysError("getting status of '%1%'", dstFile); + } } else { - struct stat dstSt; - auto res = lstat(dstFile.c_str(), &dstSt); - if (res == 0) { + auto dstStOpt = maybeLstat(dstFile.c_str()); + if (dstStOpt) { + auto & dstSt = *dstStOpt; if (S_ISLNK(dstSt.st_mode)) { auto prevPriority = state.priorities[dstFile]; if (prevPriority == priority) @@ -104,8 +103,7 @@ static void createLinks(State & state, const Path & srcDir, const Path & dstDir, throw SysError("unlinking '%1%'", dstFile); } else if (S_ISDIR(dstSt.st_mode)) throw Error("collision between non-directory '%1%' and directory '%2%'", srcFile, dstFile); - } else if (errno != ENOENT) - throw SysError("getting status of '%1%'", dstFile); + } } createSymlink(srcFile, dstFile); diff --git a/src/libstore/daemon.cc b/src/libstore/daemon.cc index 2c808015d..def2c80b2 100644 --- a/src/libstore/daemon.cc +++ b/src/libstore/daemon.cc @@ -1,5 +1,6 @@ #include "daemon.hh" #include "monitor-fd.hh" +#include "signals.hh" #include "worker-protocol.hh" #include "worker-protocol-impl.hh" #include "build-result.hh" @@ -1038,7 +1039,7 @@ void processConnection( unsigned int opCount = 0; Finally finally([&]() { - _isInterrupted = false; + setInterrupted(false); printMsgUsing(prevLogger, lvlDebug, "%d operations", opCount); }); diff --git a/src/libstore/derivations.cc b/src/libstore/derivations.cc index df14e979f..fcf813a37 100644 --- a/src/libstore/derivations.cc +++ b/src/libstore/derivations.cc @@ -1239,16 +1239,14 @@ DerivationOutput DerivationOutput::fromJSON( const ExperimentalFeatureSettings & xpSettings) { std::set keys; - ensureType(_json, nlohmann::detail::value_t::object); - auto json = (std::map) _json; + auto & json = getObject(_json); for (const auto & [key, _] : json) keys.insert(key); auto methodAlgo = [&]() -> std::pair { - std::string hashAlgoStr = json["hashAlgo"]; - // remaining to parse, will be mutated by parsers - std::string_view s = hashAlgoStr; + auto & str = getString(valueAt(json, "hashAlgo")); + std::string_view s = str; ContentAddressMethod method = ContentAddressMethod::parsePrefix(s); if (method == TextIngestionMethod {}) xpSettings.require(Xp::DynamicDerivations); @@ -1258,7 +1256,7 @@ DerivationOutput DerivationOutput::fromJSON( if (keys == (std::set { "path" })) { return DerivationOutput::InputAddressed { - .path = store.parseStorePath((std::string) json["path"]), + .path = store.parseStorePath(getString(valueAt(json, "path"))), }; } @@ -1267,10 +1265,10 @@ DerivationOutput DerivationOutput::fromJSON( auto dof = DerivationOutput::CAFixed { .ca = ContentAddress { .method = std::move(method), - .hash = Hash::parseNonSRIUnprefixed((std::string) json["hash"], hashAlgo), + .hash = Hash::parseNonSRIUnprefixed(getString(valueAt(json, "hash")), hashAlgo), }, }; - if (dof.path(store, drvName, outputName) != store.parseStorePath((std::string) json["path"])) + if (dof.path(store, drvName, outputName) != store.parseStorePath(getString(valueAt(json, "path")))) throw Error("Path doesn't match derivation output"); return dof; } @@ -1357,20 +1355,19 @@ nlohmann::json Derivation::toJSON(const StoreDirConfig & store) const Derivation Derivation::fromJSON( const StoreDirConfig & store, - const nlohmann::json & json, + const nlohmann::json & _json, const ExperimentalFeatureSettings & xpSettings) { using nlohmann::detail::value_t; Derivation res; - ensureType(json, value_t::object); + auto & json = getObject(_json); - res.name = ensureType(valueAt(json, "name"), value_t::string); + res.name = getString(valueAt(json, "name")); try { - auto & outputsObj = ensureType(valueAt(json, "outputs"), value_t::object); - for (auto & [outputName, output] : outputsObj.items()) { + for (auto & [outputName, output] : getObject(valueAt(json, "outputs"))) { res.outputs.insert_or_assign( outputName, DerivationOutput::fromJSON(store, res.name, outputName, output)); @@ -1381,8 +1378,7 @@ Derivation Derivation::fromJSON( } try { - auto & inputsList = ensureType(valueAt(json, "inputSrcs"), value_t::array); - for (auto & input : inputsList) + for (auto & input : getArray(valueAt(json, "inputSrcs"))) res.inputSrcs.insert(store.parseStorePath(static_cast(input))); } catch (Error & e) { e.addTrace({}, "while reading key 'inputSrcs'"); @@ -1391,18 +1387,17 @@ Derivation Derivation::fromJSON( try { std::function::ChildNode(const nlohmann::json &)> doInput; - doInput = [&](const auto & json) { + doInput = [&](const auto & _json) { + auto & json = getObject(_json); DerivedPathMap::ChildNode node; - node.value = static_cast( - ensureType(valueAt(json, "outputs"), value_t::array)); - for (auto & [outputId, childNode] : ensureType(valueAt(json, "dynamicOutputs"), value_t::object).items()) { + node.value = getStringSet(valueAt(json, "outputs")); + for (auto & [outputId, childNode] : getObject(valueAt(json, "dynamicOutputs"))) { xpSettings.require(Xp::DynamicDerivations); node.childMap[outputId] = doInput(childNode); } return node; }; - auto & inputDrvsObj = ensureType(valueAt(json, "inputDrvs"), value_t::object); - for (auto & [inputDrvPath, inputOutputs] : inputDrvsObj.items()) + for (auto & [inputDrvPath, inputOutputs] : getObject(valueAt(json, "inputDrvs"))) res.inputDrvs.map[store.parseStorePath(inputDrvPath)] = doInput(inputOutputs); } catch (Error & e) { @@ -1410,10 +1405,10 @@ Derivation Derivation::fromJSON( throw; } - res.platform = ensureType(valueAt(json, "system"), value_t::string); - res.builder = ensureType(valueAt(json, "builder"), value_t::string); - res.args = ensureType(valueAt(json, "args"), value_t::array); - res.env = ensureType(valueAt(json, "env"), value_t::object); + res.platform = getString(valueAt(json, "system")); + res.builder = getString(valueAt(json, "builder")); + res.args = getStringList(valueAt(json, "args")); + res.env = getStringMap(valueAt(json, "env")); return res; } diff --git a/src/libstore/filetransfer.cc b/src/libstore/filetransfer.cc index bab21bf51..df89b5bd1 100644 --- a/src/libstore/filetransfer.cc +++ b/src/libstore/filetransfer.cc @@ -1,5 +1,4 @@ #include "filetransfer.hh" -#include "namespaces.hh" #include "globals.hh" #include "store-api.hh" #include "s3.hh" @@ -12,6 +11,10 @@ #include #endif +#if __linux__ +# include "namespaces.hh" +#endif + #include #include @@ -255,11 +258,11 @@ struct curlFileTransfer : public FileTransfer int progressCallback(double dltotal, double dlnow) { try { - act.progress(dlnow, dltotal); + act.progress(dlnow, dltotal); } catch (nix::Interrupted &) { - assert(_isInterrupted); + assert(getInterrupted()); } - return _isInterrupted; + return getInterrupted(); } static int progressCallbackWrapper(void * userp, double dltotal, double dlnow, double ultotal, double ulnow) @@ -463,7 +466,7 @@ struct curlFileTransfer : public FileTransfer if (errorSink) response = std::move(errorSink->s); auto exc = - code == CURLE_ABORTED_BY_CALLBACK && _isInterrupted + code == CURLE_ABORTED_BY_CALLBACK && getInterrupted() ? FileTransferError(Interrupted, std::move(response), "%s of '%s' was interrupted", request.verb(), request.uri) : httpStatus != 0 ? FileTransferError(err, @@ -568,7 +571,9 @@ struct curlFileTransfer : public FileTransfer stopWorkerThread(); }); + #if __linux__ unshareFilesystem(); + #endif std::map> items; diff --git a/src/libstore/globals.cc b/src/libstore/globals.cc index fa0938d7b..4229fb4df 100644 --- a/src/libstore/globals.cc +++ b/src/libstore/globals.cc @@ -2,7 +2,6 @@ #include "current-process.hh" #include "archive.hh" #include "args.hh" -#include "users.hh" #include "abstract-setting-to-json.hh" #include "compute-levels.hh" @@ -57,7 +56,7 @@ Settings::Settings() , nixManDir(canonPath(NIX_MAN_DIR)) , nixDaemonSocketFile(canonPath(getEnvNonEmpty("NIX_DAEMON_SOCKET_PATH").value_or(nixStateDir + DEFAULT_SOCKET_PATH))) { - buildUsersGroup = getuid() == 0 ? "nixbld" : ""; + buildUsersGroup = isRootUser() ? "nixbld" : ""; allowSymlinkedStore = getEnv("NIX_IGNORE_SYMLINK_STORE") == "1"; auto sslOverride = getEnv("NIX_SSL_CERT_FILE").value_or(getEnv("SSL_CERT_FILE").value_or("")); @@ -346,6 +345,12 @@ void initPlugins() dlopen(file.c_str(), RTLD_LAZY | RTLD_LOCAL); if (!handle) throw Error("could not dynamically open plugin file '%s': %s", file, dlerror()); + + /* Older plugins use a statically initialized object to run their code. + Newer plugins can also export nix_plugin_entry() */ + void (*nix_plugin_entry)() = (void (*)())dlsym(handle, "nix_plugin_entry"); + if (nix_plugin_entry) + nix_plugin_entry(); } } @@ -404,6 +409,7 @@ void assertLibStoreInitialized() { } void initLibStore() { + if (initLibStoreDone) return; initLibUtil(); diff --git a/src/libstore/globals.hh b/src/libstore/globals.hh index e6544976a..4bdbe3333 100644 --- a/src/libstore/globals.hh +++ b/src/libstore/globals.hh @@ -5,6 +5,7 @@ #include "config.hh" #include "environment-variables.hh" #include "experimental-features.hh" +#include "users.hh" #include #include @@ -665,7 +666,7 @@ public: Setting sandboxFallback{this, true, "sandbox-fallback", "Whether to disable sandboxing when the kernel doesn't allow it."}; - Setting requireDropSupplementaryGroups{this, getuid() == 0, "require-drop-supplementary-groups", + Setting requireDropSupplementaryGroups{this, isRootUser(), "require-drop-supplementary-groups", R"( Following the principle of least privilege, Nix will attempt to drop supplementary groups when building with sandboxing. @@ -687,16 +688,36 @@ public: Setting sandboxShmSize{ this, "50%", "sandbox-dev-shm-size", R"( - This option determines the maximum size of the `tmpfs` filesystem - mounted on `/dev/shm` in Linux sandboxes. For the format, see the - description of the `size` option of `tmpfs` in mount(8). The default - is `50%`. + *Linux only* + + This option determines the maximum size of the `tmpfs` filesystem + mounted on `/dev/shm` in Linux sandboxes. For the format, see the + description of the `size` option of `tmpfs` in mount(8). The default + is `50%`. )"}; Setting sandboxBuildDir{this, "/build", "sandbox-build-dir", - "The build directory inside the sandbox."}; + R"( + *Linux only* + + The build directory inside the sandbox. + + This directory is backed by [`build-dir`](#conf-build-dir) on the host. + )"}; #endif + Setting> buildDir{this, std::nullopt, "build-dir", + R"( + The directory on the host, in which derivations' temporary build directories are created. + + If not set, Nix will use the system temporary directory indicated by the `TMPDIR` environment variable. + Note that builds are often performed by the Nix daemon, so its `TMPDIR` is used, and not that of the Nix command line interface. + + This is also the location where [`--keep-failed`](@docroot@/command-ref/opt-common.md#opt-keep-failed) leaves its files. + + If Nix runs without sandbox, or if the platform does not support sandboxing with bind mounts (e.g. macOS), then the [`builder`](@docroot@/language/derivations.md#attr-builder)'s environment will contain this directory, instead of the virtual location [`sandbox-build-dir`](#conf-sandbox-build-dir). + )"}; + Setting allowedImpureHostPrefixes{this, {}, "allowed-impure-host-deps", "Which prefixes to allow derivations to ask for access to (primarily for Darwin)."}; @@ -1136,9 +1157,10 @@ public: this, {}, "plugin-files", R"( A list of plugin files to be loaded by Nix. Each of these files will - be dlopened by Nix, allowing them to affect execution through static - initialization. In particular, these plugins may construct static - instances of RegisterPrimOp to add new primops or constants to the + be dlopened by Nix. If they contain the symbol `nix_plugin_entry()`, + this symbol will be called. Alternatively, they can affect execution + through static initialization. In particular, these plugins may construct + static instances of RegisterPrimOp to add new primops or constants to the expression language, RegisterStoreImplementation to add new store implementations, RegisterCommand to add new subcommands to the `nix` command, and RegisterSetting to add new nix config settings. See the diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc index 1bbeaa912..dcea46bc1 100644 --- a/src/libstore/local-store.cc +++ b/src/libstore/local-store.cc @@ -16,6 +16,7 @@ #include "posix-fs-canonicalise.hh" #include "posix-source-accessor.hh" #include "keys.hh" +#include "users.hh" #include #include @@ -223,7 +224,7 @@ LocalStore::LocalStore(const Params & params) /* Optionally, create directories and set permissions for a multi-user install. */ - if (getuid() == 0 && settings.buildUsersGroup != "") { + if (isRootUser() && settings.buildUsersGroup != "") { mode_t perm = 01775; struct group * gr = getgrnam(settings.buildUsersGroup.get().c_str()); @@ -552,6 +553,19 @@ void LocalStore::openDB(State & state, bool create) sqlite3_exec(db, ("pragma main.journal_mode = " + mode + ";").c_str(), 0, 0, 0) != SQLITE_OK) SQLiteError::throw_(db, "setting journal mode"); + if (mode == "wal") { + /* persist the WAL files when the db connection is closed. This allows + for read-only connections without write permissions on the + containing directory to succeed on a closed db. Setting the + journal_size_limit to 2^40 bytes results in the WAL files getting + truncated to 0 on exit and limits the on disk size of the WAL files + to 2^40 bytes following a checkpoint */ + if (sqlite3_exec(db, "pragma main.journal_size_limit = 1099511627776;", 0, 0, 0) == SQLITE_OK) { + int enable = 1; + sqlite3_file_control(db, NULL, SQLITE_FCNTL_PERSIST_WAL, &enable); + } + } + /* Increase the auto-checkpoint interval to 40000 pages. This seems enough to ensure that instantiating the NixOS system derivation is done in a single fsync(). */ @@ -573,7 +587,7 @@ void LocalStore::openDB(State & state, bool create) void LocalStore::makeStoreWritable() { #if __linux__ - if (getuid() != 0) return; + if (!isRootUser()) return; /* Check if /nix/store is on a read-only mount. */ struct statvfs stat; if (statvfs(realStoreDir.get().c_str(), &stat) != 0) @@ -1570,7 +1584,7 @@ static void makeMutable(const Path & path) /* Upgrade from schema 6 (Nix 0.15) to schema 7 (Nix >= 1.3). */ void LocalStore::upgradeStore7() { - if (getuid() != 0) return; + if (!isRootUser()) return; printInfo("removing immutable bits from the Nix store (this may take a while)..."); makeMutable(realStoreDir); } diff --git a/src/libstore/local.mk b/src/libstore/local.mk index f86643849..ccb7aeee2 100644 --- a/src/libstore/local.mk +++ b/src/libstore/local.mk @@ -5,12 +5,15 @@ libstore_NAME = libnixstore libstore_DIR := $(d) libstore_SOURCES := $(wildcard $(d)/*.cc $(d)/builtins/*.cc $(d)/build/*.cc) +ifdef HOST_UNIX + libstore_SOURCES += $(wildcard $(d)/unix/*.cc) +endif libstore_LIBS = libutil libstore_LDFLAGS += $(SQLITE3_LIBS) $(LIBCURL_LIBS) $(THREAD_LDFLAGS) ifdef HOST_LINUX - libstore_LDFLAGS += -ldl + libstore_LDFLAGS += -ldl endif $(foreach file,$(libstore_FILES),$(eval $(call install-data-in,$(d)/$(file),$(datadir)/nix/sandbox))) @@ -27,8 +30,15 @@ ifeq ($(HAVE_SECCOMP), 1) libstore_LDFLAGS += $(LIBSECCOMP_LIBS) endif +# Not just for this library itself, but also for downstream libraries using this library + +INCLUDE_libstore := -I $(d) -I $(d)/build +ifdef HOST_UNIX + INCLUDE_libstore += -I $(d)/unix +endif + libstore_CXXFLAGS += \ - -I src/libutil -I src/libstore -I src/libstore/build \ + $(INCLUDE_libutil) $(INCLUDE_libstore) $(INCLUDE_libstore) \ -DNIX_PREFIX=\"$(prefix)\" \ -DNIX_STORE_DIR=\"$(storedir)\" \ -DNIX_DATA_DIR=\"$(datadir)\" \ diff --git a/src/libstore/lock.cc b/src/libstore/lock.cc index 87f55ce49..023c74e34 100644 --- a/src/libstore/lock.cc +++ b/src/libstore/lock.cc @@ -2,6 +2,7 @@ #include "file-system.hh" #include "globals.hh" #include "pathlocks.hh" +#include "users.hh" #include #include @@ -192,10 +193,10 @@ std::unique_ptr acquireUserLock(uid_t nrIds, bool useUserNamespace) bool useBuildUsers() { #if __linux__ - static bool b = (settings.buildUsersGroup != "" || settings.autoAllocateUids) && getuid() == 0; + static bool b = (settings.buildUsersGroup != "" || settings.autoAllocateUids) && isRootUser(); return b; #elif __APPLE__ - static bool b = settings.buildUsersGroup != "" && getuid() == 0; + static bool b = settings.buildUsersGroup != "" && isRootUser(); return b; #else return false; diff --git a/src/libstore/nar-info.cc b/src/libstore/nar-info.cc index d9618d04c..0d219a489 100644 --- a/src/libstore/nar-info.cc +++ b/src/libstore/nar-info.cc @@ -172,19 +172,18 @@ NarInfo NarInfo::fromJSON( }; if (json.contains("url")) - res.url = ensureType(valueAt(json, "url"), value_t::string); + res.url = getString(valueAt(json, "url")); if (json.contains("compression")) - res.compression = ensureType(valueAt(json, "compression"), value_t::string); + res.compression = getString(valueAt(json, "compression")); if (json.contains("downloadHash")) res.fileHash = Hash::parseAny( - static_cast( - ensureType(valueAt(json, "downloadHash"), value_t::string)), + getString(valueAt(json, "downloadHash")), std::nullopt); if (json.contains("downloadSize")) - res.fileSize = ensureType(valueAt(json, "downloadSize"), value_t::number_integer); + res.fileSize = getInteger(valueAt(json, "downloadSize")); return res; } diff --git a/src/libstore/path-info.cc b/src/libstore/path-info.cc index d82ccd0c9..6523cb425 100644 --- a/src/libstore/path-info.cc +++ b/src/libstore/path-info.cc @@ -190,23 +190,18 @@ nlohmann::json UnkeyedValidPathInfo::toJSON( UnkeyedValidPathInfo UnkeyedValidPathInfo::fromJSON( const Store & store, - const nlohmann::json & json) + const nlohmann::json & _json) { - using nlohmann::detail::value_t; - UnkeyedValidPathInfo res { Hash(Hash::dummy), }; - ensureType(json, value_t::object); - res.narHash = Hash::parseAny( - static_cast( - ensureType(valueAt(json, "narHash"), value_t::string)), - std::nullopt); - res.narSize = ensureType(valueAt(json, "narSize"), value_t::number_integer); + auto & json = getObject(_json); + res.narHash = Hash::parseAny(getString(valueAt(json, "narHash")), std::nullopt); + res.narSize = getInteger(valueAt(json, "narSize")); try { - auto & references = ensureType(valueAt(json, "references"), value_t::array); + auto references = getStringList(valueAt(json, "references")); for (auto & input : references) res.references.insert(store.parseStorePath(static_cast (input))); @@ -216,20 +211,16 @@ UnkeyedValidPathInfo UnkeyedValidPathInfo::fromJSON( } if (json.contains("ca")) - res.ca = ContentAddress::parse( - static_cast( - ensureType(valueAt(json, "ca"), value_t::string))); + res.ca = ContentAddress::parse(getString(valueAt(json, "ca"))); if (json.contains("deriver")) - res.deriver = store.parseStorePath( - static_cast( - ensureType(valueAt(json, "deriver"), value_t::string))); + res.deriver = store.parseStorePath(getString(valueAt(json, "deriver"))); if (json.contains("registrationTime")) - res.registrationTime = ensureType(valueAt(json, "registrationTime"), value_t::number_integer); + res.registrationTime = getInteger(valueAt(json, "registrationTime")); if (json.contains("ultimate")) - res.ultimate = ensureType(valueAt(json, "ultimate"), value_t::boolean); + res.ultimate = getBoolean(valueAt(json, "ultimate")); if (json.contains("signatures")) res.sigs = valueAt(json, "signatures"); diff --git a/src/libstore/profiles.cc b/src/libstore/profiles.cc index e8b88693d..73d3976f4 100644 --- a/src/libstore/profiles.cc +++ b/src/libstore/profiles.cc @@ -308,7 +308,7 @@ std::string optimisticLockProfile(const Path & profile) Path profilesDir() { auto profileRoot = - (getuid() == 0) + isRootUser() ? rootProfilesDir() : createNixStateDir() + "/profiles"; createDirs(profileRoot); @@ -332,7 +332,7 @@ Path getDefaultProfile() // Backwards compatibiliy measure: Make root's profile available as // `.../default` as it's what NixOS and most of the init scripts expect Path globalProfileLink = settings.nixStateDir + "/profiles/default"; - if (getuid() == 0 && !pathExists(globalProfileLink)) { + if (isRootUser() && !pathExists(globalProfileLink)) { replaceSymlink(profile, globalProfileLink); } return absPath(readLink(profileLink), dirOf(profileLink)); diff --git a/src/libstore/sqlite.cc b/src/libstore/sqlite.cc index 06abfb90b..3175c1978 100644 --- a/src/libstore/sqlite.cc +++ b/src/libstore/sqlite.cc @@ -7,6 +7,7 @@ #include #include +#include namespace nix { @@ -256,10 +257,8 @@ void handleSQLiteBusy(const SQLiteBusy & e, time_t & nextWarning) /* Sleep for a while since retrying the transaction right away is likely to fail again. */ checkInterrupt(); - struct timespec t; - t.tv_sec = 0; - t.tv_nsec = (random() % 100) * 1000 * 1000; /* <= 0.1s */ - nanosleep(&t, 0); + /* <= 0.1s */ + std::this_thread::sleep_for(std::chrono::milliseconds { rand() % 100 }); } } diff --git a/src/libstore/store-api.cc b/src/libstore/store-api.cc index 4356296d4..62403e633 100644 --- a/src/libstore/store-api.cc +++ b/src/libstore/store-api.cc @@ -1307,7 +1307,7 @@ std::shared_ptr openFromNonUri(const std::string & uri, const Store::Para #if __linux__ else if (!pathExists(stateDir) && params.empty() - && getuid() != 0 + && !isRootUser() && !getEnv("NIX_STORE_DIR").has_value() && !getEnv("NIX_STATE_DIR").has_value()) { diff --git a/src/libutil-c/local.mk b/src/libutil-c/local.mk new file mode 100644 index 000000000..f2df1ef43 --- /dev/null +++ b/src/libutil-c/local.mk @@ -0,0 +1,18 @@ +libraries += libutilc + +libutilc_NAME = libnixutilc + +libutilc_DIR := $(d) + +libutilc_SOURCES := $(wildcard $(d)/*.cc) + +# Not just for this library itself, but also for downstream libraries using this library + +INCLUDE_libutilc := -I $(d) +libutilc_CXXFLAGS += $(INCLUDE_libutil) $(INCLUDE_libutilc) + +libutilc_LIBS = libutil + +libutilc_LDFLAGS += $(THREAD_LDFLAGS) + +libutilc_FORCE_INSTALL := 1 diff --git a/src/libutil-c/nix_api_util.cc b/src/libutil-c/nix_api_util.cc new file mode 100644 index 000000000..8d0f7ac38 --- /dev/null +++ b/src/libutil-c/nix_api_util.cc @@ -0,0 +1,148 @@ +#include "nix_api_util.h" +#include "config.hh" +#include "error.hh" +#include "nix_api_util_internal.h" +#include "util.hh" + +#include +#include + +nix_c_context * nix_c_context_create() +{ + return new nix_c_context(); +} + +void nix_c_context_free(nix_c_context * context) +{ + delete context; +} + +nix_err nix_context_error(nix_c_context * context) +{ + if (context == nullptr) { + throw; + } + try { + throw; + } catch (nix::Error & e) { + /* Storing this exception is annoying, take what we need here */ + context->last_err = e.what(); + context->info = e.info(); + int status; + const char * demangled = abi::__cxa_demangle(typeid(e).name(), 0, 0, &status); + if (demangled) { + context->name = demangled; + // todo: free(demangled); + } else { + context->name = typeid(e).name(); + } + context->last_err_code = NIX_ERR_NIX_ERROR; + return context->last_err_code; + } catch (const std::exception & e) { + context->last_err = e.what(); + context->last_err_code = NIX_ERR_UNKNOWN; + return context->last_err_code; + } + // unreachable +} + +nix_err nix_set_err_msg(nix_c_context * context, nix_err err, const char * msg) +{ + if (context == nullptr) { + // todo last_err_code + throw nix::Error("Nix C api error: %s", msg); + } + context->last_err_code = err; + context->last_err = msg; + return err; +} + +const char * nix_version_get() +{ + return PACKAGE_VERSION; +} + +// Implementations + +nix_err nix_setting_get(nix_c_context * context, const char * key, void * callback, void * user_data) +{ + if (context) + context->last_err_code = NIX_OK; + try { + std::map settings; + nix::globalConfig.getSettings(settings); + if (settings.contains(key)) { + return call_nix_get_string_callback(settings[key].value, callback, user_data); + } else { + return nix_set_err_msg(context, NIX_ERR_KEY, "Setting not found"); + } + } + NIXC_CATCH_ERRS +} + +nix_err nix_setting_set(nix_c_context * context, const char * key, const char * value) +{ + if (context) + context->last_err_code = NIX_OK; + if (nix::globalConfig.set(key, value)) + return NIX_OK; + else { + return nix_set_err_msg(context, NIX_ERR_KEY, "Setting not found"); + } +} + +nix_err nix_libutil_init(nix_c_context * context) +{ + if (context) + context->last_err_code = NIX_OK; + try { + nix::initLibUtil(); + return NIX_OK; + } + NIXC_CATCH_ERRS +} + +const char * nix_err_msg(nix_c_context * context, const nix_c_context * read_context, unsigned int * n) +{ + if (context) + context->last_err_code = NIX_OK; + if (read_context->last_err) { + if (n) + *n = read_context->last_err->size(); + return read_context->last_err->c_str(); + } + nix_set_err_msg(context, NIX_ERR_UNKNOWN, "No error message"); + return nullptr; +} + +nix_err nix_err_name(nix_c_context * context, const nix_c_context * read_context, void * callback, void * user_data) +{ + if (context) + context->last_err_code = NIX_OK; + if (read_context->last_err_code != NIX_ERR_NIX_ERROR) { + return nix_set_err_msg(context, NIX_ERR_UNKNOWN, "Last error was not a nix error"); + } + return call_nix_get_string_callback(read_context->name, callback, user_data); +} + +nix_err nix_err_info_msg(nix_c_context * context, const nix_c_context * read_context, void * callback, void * user_data) +{ + if (context) + context->last_err_code = NIX_OK; + if (read_context->last_err_code != NIX_ERR_NIX_ERROR) { + return nix_set_err_msg(context, NIX_ERR_UNKNOWN, "Last error was not a nix error"); + } + return call_nix_get_string_callback(read_context->info->msg.str(), callback, user_data); +} + +nix_err nix_err_code(const nix_c_context * read_context) +{ + return read_context->last_err_code; +} + +// internal +nix_err call_nix_get_string_callback(const std::string str, void * callback, void * user_data) +{ + ((nix_get_string_callback) callback)(str.c_str(), str.size(), user_data); + return NIX_OK; +} diff --git a/src/libutil-c/nix_api_util.h b/src/libutil-c/nix_api_util.h new file mode 100644 index 000000000..cb506ca90 --- /dev/null +++ b/src/libutil-c/nix_api_util.h @@ -0,0 +1,301 @@ +#ifndef NIX_API_UTIL_H +#define NIX_API_UTIL_H +/** + * @defgroup libutil libutil + * @brief C bindings for nix libutil + * + * libutil is used for functionality shared between + * different Nix modules. + * @{ + */ +/** @file + * @brief Main entry for the libutil C bindings + * + * Also contains error handling utilities + */ + +#ifdef __cplusplus +extern "C" { +#endif +// cffi start + +/** @defgroup errors Handling errors + * @brief Dealing with errors from the Nix side + * + * To handle errors that can be returned from the Nix API, + * a nix_c_context can be passed to any function that potentially returns an + * error. + * + * Error information will be stored in this context, and can be retrieved + * using nix_err_code and nix_err_msg. + * + * Passing NULL instead will cause the API to throw C++ errors. + * + * Example: + * @code{.c} + * int main() { + * nix_c_context* ctx = nix_c_context_create(); + * nix_libutil_init(ctx); + * if (nix_err_code(ctx) != NIX_OK) { + * printf("error: %s\n", nix_err_msg(NULL, ctx, NULL)); + * return 1; + * } + * return 0; + * } + * @endcode + * @{ + */ +// Error codes +/** + * @brief Type for error codes in the NIX system + * + * This type can have one of several predefined constants: + * - NIX_OK: No error occurred (0) + * - NIX_ERR_UNKNOWN: An unknown error occurred (-1) + * - NIX_ERR_OVERFLOW: An overflow error occurred (-2) + * - NIX_ERR_KEY: A key error occurred (-3) + * - NIX_ERR_NIX_ERROR: A generic Nix error occurred (-4) + */ +typedef int nix_err; + +/** + * @brief No error occurred. + * + * This error code is returned when no error has occurred during the function + * execution. + */ +#define NIX_OK 0 + +/** + * @brief An unknown error occurred. + * + * This error code is returned when an unknown error occurred during the + * function execution. + */ +#define NIX_ERR_UNKNOWN -1 + +/** + * @brief An overflow error occurred. + * + * This error code is returned when an overflow error occurred during the + * function execution. + */ +#define NIX_ERR_OVERFLOW -2 + +/** + * @brief A key error occurred. + * + * This error code is returned when a key error occurred during the function + * execution. + */ +#define NIX_ERR_KEY -3 + +/** + * @brief A generic Nix error occurred. + * + * This error code is returned when a generic Nix error occurred during the + * function execution. + */ +#define NIX_ERR_NIX_ERROR -4 + +/** + * @brief This object stores error state. + * @struct nix_c_context + * + * Passed as a first parameter to functions that can fail, to store error + * information. + * + * Optional wherever it can be used, passing NULL instead will throw a C++ + * exception. + * + * The struct is laid out so that it can also be cast to nix_err* to inspect + * directly: + * @code{.c} + * assert(*(nix_err*)ctx == NIX_OK); + * @endcode + * @note These can be reused between different function calls, + * but make sure not to use them for multiple calls simultaneously (which can + * happen in callbacks). + */ +typedef struct nix_c_context nix_c_context; + +/** + * @brief Called to get the value of a string owned by Nix. + * + * @param[in] start the string to copy. + * @param[in] n the string length. + * @param[in] user_data optional, arbitrary data, passed to the nix_get_string_callback when it's called. + */ +typedef void (*nix_get_string_callback)(const char * start, unsigned int n, void * user_data); + +// Function prototypes + +/** + * @brief Allocate a new nix_c_context. + * @throws std::bad_alloc + * @return allocated nix_c_context, owned by the caller. Free using + * `nix_c_context_free`. + */ +nix_c_context * nix_c_context_create(); +/** + * @brief Free a nix_c_context. Does not fail. + * @param[out] context The context to free, mandatory. + */ +void nix_c_context_free(nix_c_context * context); +/** + * @} + */ + +/** + * @brief Initializes nix_libutil and its dependencies. + * + * This function can be called multiple times, but should be called at least + * once prior to any other nix function. + * + * @param[out] context Optional, stores error information + * @return NIX_OK if the initialization is successful, or an error code + * otherwise. + */ +nix_err nix_libutil_init(nix_c_context * context); + +/** @defgroup settings + * @{ + */ +/** + * @brief Retrieves a setting from the nix global configuration. + * + * This function requires nix_libutil_init() to be called at least once prior to + * its use. + * + * @param[out] context optional, Stores error information + * @param[in] key The key of the setting to retrieve. + * @param[in] callback Called with the setting value. + * @param[in] user_data optional, arbitrary data, passed to the callback when it's called. + * @see nix_get_string_callback + * @return NIX_ERR_KEY if the setting is unknown, or NIX_OK if the setting was retrieved + * successfully. + */ +nix_err nix_setting_get(nix_c_context * context, const char * key, void * callback, void * user_data); + +/** + * @brief Sets a setting in the nix global configuration. + * + * Use "extra-" to append to the setting's value. + * + * Settings only apply for new State%s. Call nix_plugins_init() when you are + * done with the settings to load any plugins. + * + * @param[out] context optional, Stores error information + * @param[in] key The key of the setting to set. + * @param[in] value The value to set for the setting. + * @return NIX_ERR_KEY if the setting is unknown, or NIX_OK if the setting was + * set successfully. + */ +nix_err nix_setting_set(nix_c_context * context, const char * key, const char * value); + +/** + * @} + */ +// todo: nix_plugins_init() + +/** + * @brief Retrieves the nix library version. + * + * Does not fail. + * @return A static string representing the version of the nix library. + */ +const char * nix_version_get(); + +/** @addtogroup errors + * @{ + */ +/** + * @brief Retrieves the most recent error message from a context. + * + * @pre This function should only be called after a previous nix function has + * returned an error. + * + * @param[out] context optional, the context to store errors in if this function + * fails + * @param[in] ctx the context to retrieve the error message from + * @param[out] n optional: a pointer to an unsigned int that is set to the + * length of the error. + * @return nullptr if no error message was ever set, + * a borrowed pointer to the error message otherwise. + */ +const char * nix_err_msg(nix_c_context * context, const nix_c_context * ctx, unsigned int * n); + +/** + * @brief Retrieves the error message from errorInfo in a context. + * + * Used to inspect nix Error messages. + * + * @pre This function should only be called after a previous nix function has + * returned a NIX_ERR_NIX_ERROR + * + * @param[out] context optional, the context to store errors in if this function + * fails + * @param[in] read_context the context to retrieve the error message from. + * @param[in] callback Called with the error message. + * @param[in] user_data optional, arbitrary data, passed to the callback when it's called. + * @see nix_get_string_callback + * @return NIX_OK if there were no errors, an error code otherwise. + */ +nix_err +nix_err_info_msg(nix_c_context * context, const nix_c_context * read_context, void * callback, void * user_data); + +/** + * @brief Retrieves the error name from a context. + * + * Used to inspect nix Error messages. + * + * @pre This function should only be called after a previous nix function has + * returned a NIX_ERR_NIX_ERROR + * + * @param context optional, the context to store errors in if this function + * fails + * @param[in] read_context the context to retrieve the error message from + * @param[in] callback Called with the error name. + * @param[in] user_data optional, arbitrary data, passed to the callback when it's called. + * @see nix_get_string_callback + * @return NIX_OK if there were no errors, an error code otherwise. + */ +nix_err nix_err_name(nix_c_context * context, const nix_c_context * read_context, void * callback, void * user_data); + +/** + * @brief Retrieves the most recent error code from a nix_c_context + * + * Equivalent to reading the first field of the context. + * + * Does not fail + * + * @param[in] read_context the context to retrieve the error message from + * @return most recent error code stored in the context. + */ +nix_err nix_err_code(const nix_c_context * read_context); + +/** + * @brief Set an error message on a nix context. + * + * This should be used when you want to throw an error from a PrimOp callback. + * + * All other use is internal to the API. + * + * @param context context to write the error message to, or NULL + * @param err The error code to set and return + * @param msg The error message to set. + * @returns the error code set + */ +nix_err nix_set_err_msg(nix_c_context * context, nix_err err, const char * msg); + +/** + * @} + */ + +// cffi end +#ifdef __cplusplus +} +#endif + +/** @} */ +#endif // NIX_API_UTIL_H diff --git a/src/libutil-c/nix_api_util_internal.h b/src/libutil-c/nix_api_util_internal.h new file mode 100644 index 000000000..6e8eac020 --- /dev/null +++ b/src/libutil-c/nix_api_util_internal.h @@ -0,0 +1,49 @@ +#ifndef NIX_API_UTIL_INTERNAL_H +#define NIX_API_UTIL_INTERNAL_H + +#include +#include + +#include "error.hh" +#include "nix_api_util.h" + +struct nix_c_context +{ + nix_err last_err_code = NIX_OK; + std::optional last_err = {}; + std::optional info = {}; + std::string name = ""; +}; + +nix_err nix_context_error(nix_c_context * context); + +/** + * Internal use only. + * + * Helper to invoke nix_get_string_callback + * @param context optional, the context to store errors in if this function + * fails + * @param str The string to observe + * @param callback Called with the observed string. + * @param user_data optional, arbitrary data, passed to the callback when it's called. + * @return NIX_OK if there were no errors. + * @see nix_get_string_callback + */ +nix_err call_nix_get_string_callback(const std::string str, void * callback, void * user_data); + +#define NIXC_CATCH_ERRS \ + catch (...) \ + { \ + return nix_context_error(context); \ + } \ + return NIX_OK; + +#define NIXC_CATCH_ERRS_RES(def) \ + catch (...) \ + { \ + nix_context_error(context); \ + return def; \ + } +#define NIXC_CATCH_ERRS_NULL NIXC_CATCH_ERRS_RES(nullptr) + +#endif // NIX_API_UTIL_INTERNAL_H diff --git a/src/libutil/args.cc b/src/libutil/args.cc index a981ed9fb..834fc7314 100644 --- a/src/libutil/args.cc +++ b/src/libutil/args.cc @@ -285,7 +285,7 @@ void RootArgs::parseCmdline(const Strings & _cmdline, bool allowShebang) std::string line; std::getline(stream,line); - static const std::string commentChars("#/\\%@*-"); + static const std::string commentChars("#/\\%@*-("); std::string shebangContent; while (std::getline(stream,line) && !line.empty() && commentChars.find(line[0]) != std::string::npos){ line = chomp(line); diff --git a/src/libutil/compression.cc b/src/libutil/compression.cc index d06f1f87b..d17401f27 100644 --- a/src/libutil/compression.cc +++ b/src/libutil/compression.cc @@ -12,8 +12,6 @@ #include #include -#include - namespace nix { static const int COMPRESSION_LEVEL_DEFAULT = -1; @@ -40,20 +38,26 @@ struct ArchiveDecompressionSource : Source { std::unique_ptr archive = 0; Source & src; - ArchiveDecompressionSource(Source & src) : src(src) {} + std::optional compressionMethod; + ArchiveDecompressionSource(Source & src, std::optional compressionMethod = std::nullopt) + : src(src) + , compressionMethod(std::move(compressionMethod)) + { + } ~ArchiveDecompressionSource() override {} - size_t read(char * data, size_t len) override { + size_t read(char * data, size_t len) override + { struct archive_entry * ae; if (!archive) { - archive = std::make_unique(src, true); - this->archive->check(archive_read_next_header(this->archive->archive, &ae), - "failed to read header (%s)"); + archive = std::make_unique(src, /*raw*/ true, compressionMethod); + this->archive->check(archive_read_next_header(this->archive->archive, &ae), "failed to read header (%s)"); if (archive_filter_count(this->archive->archive) < 2) { throw CompressionError("input compression not recognized"); } } ssize_t result = archive_read_data(this->archive->archive, data, len); - if (result > 0) return result; + if (result > 0) + return result; if (result == 0) { throw EndOfFile("reached end of compressed file"); } @@ -67,16 +71,19 @@ struct ArchiveCompressionSink : CompressionSink Sink & nextSink; struct archive * archive; - ArchiveCompressionSink(Sink & nextSink, std::string format, bool parallel, int level = COMPRESSION_LEVEL_DEFAULT) : nextSink(nextSink) + ArchiveCompressionSink(Sink & nextSink, std::string format, bool parallel, int level = COMPRESSION_LEVEL_DEFAULT) + : nextSink(nextSink) { archive = archive_write_new(); - if (!archive) throw Error("failed to initialize libarchive"); + if (!archive) + throw Error("failed to initialize libarchive"); check(archive_write_add_filter_by_name(archive, format.c_str()), "couldn't initialize compression (%s)"); check(archive_write_set_format_raw(archive)); if (parallel) check(archive_write_set_filter_option(archive, format.c_str(), "threads", "0")); if (level != COMPRESSION_LEVEL_DEFAULT) - check(archive_write_set_filter_option(archive, format.c_str(), "compression-level", std::to_string(level).c_str())); + check(archive_write_set_filter_option( + archive, format.c_str(), "compression-level", std::to_string(level).c_str())); // disable internal buffering check(archive_write_set_bytes_per_block(archive, 0)); // disable output padding @@ -86,7 +93,8 @@ struct ArchiveCompressionSink : CompressionSink ~ArchiveCompressionSink() override { - if (archive) archive_write_free(archive); + if (archive) + archive_write_free(archive); } void finish() override @@ -106,7 +114,8 @@ struct ArchiveCompressionSink : CompressionSink void writeUnbuffered(std::string_view data) override { ssize_t result = archive_write_data(archive, data.data(), data.length()); - if (result <= 0) check(result); + if (result <= 0) + check(result); } private: @@ -130,13 +139,20 @@ private: struct NoneSink : CompressionSink { Sink & nextSink; - NoneSink(Sink & nextSink, int level = COMPRESSION_LEVEL_DEFAULT) : nextSink(nextSink) + NoneSink(Sink & nextSink, int level = COMPRESSION_LEVEL_DEFAULT) + : nextSink(nextSink) { if (level != COMPRESSION_LEVEL_DEFAULT) warn("requested compression level '%d' not supported by compression method 'none'", level); } - void finish() override { flush(); } - void writeUnbuffered(std::string_view data) override { nextSink(data); } + void finish() override + { + flush(); + } + void writeUnbuffered(std::string_view data) override + { + nextSink(data); + } }; struct BrotliDecompressionSink : ChunkedCompressionSink @@ -145,7 +161,8 @@ struct BrotliDecompressionSink : ChunkedCompressionSink BrotliDecoderState * state; bool finished = false; - BrotliDecompressionSink(Sink & nextSink) : nextSink(nextSink) + BrotliDecompressionSink(Sink & nextSink) + : nextSink(nextSink) { state = BrotliDecoderCreateInstance(nullptr, nullptr, nullptr); if (!state) @@ -173,10 +190,7 @@ struct BrotliDecompressionSink : ChunkedCompressionSink while (!finished && (!data.data() || avail_in)) { checkInterrupt(); - if (!BrotliDecoderDecompressStream(state, - &avail_in, &next_in, - &avail_out, &next_out, - nullptr)) + if (!BrotliDecoderDecompressStream(state, &avail_in, &next_in, &avail_out, &next_out, nullptr)) throw CompressionError("error while decompressing brotli file"); if (avail_out < sizeof(outbuf) || avail_in == 0) { @@ -206,8 +220,8 @@ std::unique_ptr makeDecompressionSink(const std::string & method, Si else if (method == "br") return std::make_unique(nextSink); else - return sourceToSink([&](Source & source) { - auto decompressionSource = std::make_unique(source); + return sourceToSink([method, &nextSink](Source & source) { + auto decompressionSource = std::make_unique(source, method); decompressionSource->drainInto(nextSink); }); } @@ -219,7 +233,8 @@ struct BrotliCompressionSink : ChunkedCompressionSink BrotliEncoderState * state; bool finished = false; - BrotliCompressionSink(Sink & nextSink) : nextSink(nextSink) + BrotliCompressionSink(Sink & nextSink) + : nextSink(nextSink) { state = BrotliEncoderCreateInstance(nullptr, nullptr, nullptr); if (!state) @@ -247,11 +262,9 @@ struct BrotliCompressionSink : ChunkedCompressionSink while (!finished && (!data.data() || avail_in)) { checkInterrupt(); - if (!BrotliEncoderCompressStream(state, - data.data() ? BROTLI_OPERATION_PROCESS : BROTLI_OPERATION_FINISH, - &avail_in, &next_in, - &avail_out, &next_out, - nullptr)) + if (!BrotliEncoderCompressStream( + state, data.data() ? BROTLI_OPERATION_PROCESS : BROTLI_OPERATION_FINISH, &avail_in, &next_in, + &avail_out, &next_out, nullptr)) throw CompressionError("error while compressing brotli compression"); if (avail_out < sizeof(outbuf) || avail_in == 0) { @@ -267,9 +280,8 @@ struct BrotliCompressionSink : ChunkedCompressionSink ref makeCompressionSink(const std::string & method, Sink & nextSink, const bool parallel, int level) { - std::vector la_supports = { - "bzip2", "compress", "grzip", "gzip", "lrzip", "lz4", "lzip", "lzma", "lzop", "xz", "zstd" - }; + std::vector la_supports = {"bzip2", "compress", "grzip", "gzip", "lrzip", "lz4", + "lzip", "lzma", "lzop", "xz", "zstd"}; if (std::find(la_supports.begin(), la_supports.end(), method) != la_supports.end()) { return make_ref(nextSink, method, parallel, level); } diff --git a/src/libutil/compression.hh b/src/libutil/compression.hh index 4e53a7b3c..e0c531b1f 100644 --- a/src/libutil/compression.hh +++ b/src/libutil/compression.hh @@ -11,7 +11,7 @@ namespace nix { struct CompressionSink : BufferedSink, FinishSink { - using BufferedSink::operator (); + using BufferedSink::operator(); using BufferedSink::writeUnbuffered; using FinishSink::finish; }; @@ -22,7 +22,8 @@ std::unique_ptr makeDecompressionSink(const std::string & method, Si std::string compress(const std::string & method, std::string_view in, const bool parallel = false, int level = -1); -ref makeCompressionSink(const std::string & method, Sink & nextSink, const bool parallel = false, int level = -1); +ref +makeCompressionSink(const std::string & method, Sink & nextSink, const bool parallel = false, int level = -1); MakeError(UnknownCompressionMethod, Error); diff --git a/src/libutil/config.cc b/src/libutil/config.cc index 5a753fcf2..2267592a8 100644 --- a/src/libutil/config.cc +++ b/src/libutil/config.cc @@ -124,7 +124,7 @@ static void applyConfigInner(const std::string & contents, const std::string & p auto p = absPath(tokens[1], dirOf(path)); if (pathExists(p)) { try { - std::string includedContents = readFile(path); + std::string includedContents = readFile(p); applyConfigInner(includedContents, p, parsedContents); } catch (SystemError &) { // TODO: Do we actually want to ignore this? Or is it better to fail? diff --git a/src/libutil/current-process.cc b/src/libutil/current-process.cc index f80f43ef0..d33f7163a 100644 --- a/src/libutil/current-process.cc +++ b/src/libutil/current-process.cc @@ -2,7 +2,6 @@ #include #include "current-process.hh" -#include "namespaces.hh" #include "util.hh" #include "finally.hh" #include "file-system.hh" @@ -17,6 +16,7 @@ # include # include # include "cgroup.hh" +# include "namespaces.hh" #endif #include @@ -82,9 +82,11 @@ void setStackSize(rlim_t stackSize) void restoreProcessContext(bool restoreMounts) { - restoreSignals(); + unix::restoreSignals(); if (restoreMounts) { + #if __linux__ restoreMountNamespace(); + #endif } if (savedStackSize) { diff --git a/src/libutil/environment-variables.cc b/src/libutil/environment-variables.cc index 6618d7872..d43197aa0 100644 --- a/src/libutil/environment-variables.cc +++ b/src/libutil/environment-variables.cc @@ -32,7 +32,6 @@ std::map getEnv() return env; } - void clearEnv() { for (auto & name : getEnv()) @@ -43,7 +42,7 @@ void replaceEnv(const std::map & newEnv) { clearEnv(); for (auto & newEnvVar : newEnv) - setenv(newEnvVar.first.c_str(), newEnvVar.second.c_str(), 1); + setEnv(newEnvVar.first.c_str(), newEnvVar.second.c_str()); } } diff --git a/src/libutil/environment-variables.hh b/src/libutil/environment-variables.hh index 21eb4619b..21c2356a4 100644 --- a/src/libutil/environment-variables.hh +++ b/src/libutil/environment-variables.hh @@ -28,6 +28,14 @@ std::optional getEnvNonEmpty(const std::string & key); */ std::map getEnv(); +/** + * Like POSIX `setenv`, but always overrides. + * + * We don't need the non-overriding version, and this is easier to + * reimplement on Windows. + */ +int setEnv(const char * name, const char * value); + /** * Clear the environment. */ diff --git a/src/libutil/error.cc b/src/libutil/error.cc index d1e864a1a..fd4f4efd1 100644 --- a/src/libutil/error.cc +++ b/src/libutil/error.cc @@ -11,14 +11,15 @@ namespace nix { -void BaseError::addTrace(std::shared_ptr && e, HintFmt hint) +void BaseError::addTrace(std::shared_ptr && e, HintFmt hint, TracePrint print) { - err.traces.push_front(Trace { .pos = std::move(e), .hint = hint }); + err.traces.push_front(Trace { .pos = std::move(e), .hint = hint, .print = print }); } -void throwExceptionSelfCheck(){ +void throwExceptionSelfCheck() +{ // This is meant to be caught in initLibUtil() - throw SysError("C++ exception handling is broken. This would appear to be a problem with the way Nix was compiled and/or linked and/or loaded."); + throw Error("C++ exception handling is broken. This would appear to be a problem with the way Nix was compiled and/or linked and/or loaded."); } // c++ std::exception descendants must have a 'const char* what()' function. @@ -163,7 +164,7 @@ static bool printPosMaybe(std::ostream & oss, std::string_view indent, const std return hasPos; } -void printTrace( +static void printTrace( std::ostream & output, const std::string_view & indent, size_t & count, @@ -379,29 +380,39 @@ std::ostream & showErrorInfo(std::ostream & out, const ErrorInfo & einfo, bool s // A consecutive sequence of stack traces that are all in `tracesSeen`. std::vector skippedTraces; size_t count = 0; + bool truncate = false; for (const auto & trace : einfo.traces) { if (trace.hint.str().empty()) continue; if (!showTrace && count > 3) { - oss << "\n" << ANSI_WARNING "(stack trace truncated; use '--show-trace' to show the full trace)" ANSI_NORMAL << "\n"; - break; + truncate = true; } - if (tracesSeen.count(trace)) { - skippedTraces.push_back(trace); - continue; + if (!truncate || trace.print == TracePrint::Always) { + + if (tracesSeen.count(trace)) { + skippedTraces.push_back(trace); + continue; + } + + tracesSeen.insert(trace); + + printSkippedTracesMaybe(oss, ellipsisIndent, count, skippedTraces, tracesSeen); + + count++; + + printTrace(oss, ellipsisIndent, count, trace); } - tracesSeen.insert(trace); - - printSkippedTracesMaybe(oss, ellipsisIndent, count, skippedTraces, tracesSeen); - - count++; - - printTrace(oss, ellipsisIndent, count, trace); } + printSkippedTracesMaybe(oss, ellipsisIndent, count, skippedTraces, tracesSeen); + + if (truncate) { + oss << "\n" << ANSI_WARNING "(stack trace truncated; use '--show-trace' to show the full, detailed trace)" ANSI_NORMAL << "\n"; + } + oss << "\n" << prefix; } diff --git a/src/libutil/error.hh b/src/libutil/error.hh index 89f5ad021..445b1e19c 100644 --- a/src/libutil/error.hh +++ b/src/libutil/error.hh @@ -61,9 +61,22 @@ void printCodeLines(std::ostream & out, const Pos & errPos, const LinesOfCode & loc); +/** + * When a stack frame is printed. + */ +enum struct TracePrint { + /** + * The default behavior; always printed when `--show-trace` is set. + */ + Default, + /** Always printed. Produced by `builtins.addErrorContext`. */ + Always, +}; + struct Trace { std::shared_ptr pos; HintFmt hint; + TracePrint print = TracePrint::Default; }; inline bool operator<(const Trace& lhs, const Trace& rhs); @@ -137,6 +150,10 @@ public: : err(e) { } + std::string message() { + return err.msg.str(); + } + const char * what() const noexcept override { return calcWhat().c_str(); } const std::string & msg() const { return calcWhat(); } const ErrorInfo & info() const { calcWhat(); return err; } @@ -161,7 +178,7 @@ public: addTrace(std::move(e), HintFmt(std::string(fs), args...)); } - void addTrace(std::shared_ptr && e, HintFmt hint); + void addTrace(std::shared_ptr && e, HintFmt hint, TracePrint print = TracePrint::Default); bool hasTrace() const { return !err.traces.empty(); } diff --git a/src/libutil/experimental-features.cc b/src/libutil/experimental-features.cc index 374e674af..4fc07afaf 100644 --- a/src/libutil/experimental-features.cc +++ b/src/libutil/experimental-features.cc @@ -203,16 +203,6 @@ constexpr std::array xpFeatureDetails )", .trackingUrl = "https://github.com/NixOS/nix/milestone/40", }, - { - .tag = Xp::ReplFlake, - .name = "repl-flake", - .description = R"( - *Enabled with [`flakes`](#xp-feature-flakes) since 2.19* - - Allow passing [installables](@docroot@/command-ref/new-cli/nix.md#installables) to `nix repl`, making its interface consistent with the other experimental commands. - )", - .trackingUrl = "https://github.com/NixOS/nix/milestone/32", - }, { .tag = Xp::AutoAllocateUids, .name = "auto-allocate-uids", diff --git a/src/libutil/experimental-features.hh b/src/libutil/experimental-features.hh index eae4fa9b8..47c21280f 100644 --- a/src/libutil/experimental-features.hh +++ b/src/libutil/experimental-features.hh @@ -26,7 +26,6 @@ enum struct ExperimentalFeature RecursiveNix, NoUrlLiterals, FetchClosure, - ReplFlake, AutoAllocateUids, Cgroups, DaemonTrustOverride, diff --git a/src/libutil/file-descriptor.cc b/src/libutil/file-descriptor.cc index 55d57e29b..95cbb8537 100644 --- a/src/libutil/file-descriptor.cc +++ b/src/libutil/file-descriptor.cc @@ -8,74 +8,14 @@ namespace nix { -std::string readFile(int fd) -{ - struct stat st; - if (fstat(fd, &st) == -1) - throw SysError("statting file"); - - return drainFD(fd, true, st.st_size); -} - - -void readFull(int fd, char * buf, size_t count) -{ - while (count) { - checkInterrupt(); - ssize_t res = read(fd, buf, count); - if (res == -1) { - if (errno == EINTR) continue; - throw SysError("reading from file"); - } - if (res == 0) throw EndOfFile("unexpected end-of-file"); - count -= res; - buf += res; - } -} - - -void writeFull(int fd, std::string_view s, bool allowInterrupts) -{ - while (!s.empty()) { - if (allowInterrupts) checkInterrupt(); - ssize_t res = write(fd, s.data(), s.size()); - if (res == -1 && errno != EINTR) - throw SysError("writing to file"); - if (res > 0) - s.remove_prefix(res); - } -} - - -std::string readLine(int fd) -{ - std::string s; - while (1) { - checkInterrupt(); - char ch; - // FIXME: inefficient - ssize_t rd = read(fd, &ch, 1); - if (rd == -1) { - if (errno != EINTR) - throw SysError("reading a line"); - } else if (rd == 0) - throw EndOfFile("unexpected EOF reading a line"); - else { - if (ch == '\n') return s; - s += ch; - } - } -} - - -void writeLine(int fd, std::string s) +void writeLine(Descriptor fd, std::string s) { s += '\n'; writeFull(fd, s); } -std::string drainFD(int fd, bool block, const size_t reserveSize) +std::string drainFD(Descriptor fd, bool block, const size_t reserveSize) { // the parser needs two extra bytes to append terminating characters, other users will // not care very much about the extra memory. @@ -85,50 +25,18 @@ std::string drainFD(int fd, bool block, const size_t reserveSize) } -void drainFD(int fd, Sink & sink, bool block) -{ - // silence GCC maybe-uninitialized warning in finally - int saved = 0; - - if (!block) { - saved = fcntl(fd, F_GETFL); - if (fcntl(fd, F_SETFL, saved | O_NONBLOCK) == -1) - throw SysError("making file descriptor non-blocking"); - } - - Finally finally([&] { - if (!block) { - if (fcntl(fd, F_SETFL, saved) == -1) - throw SysError("making file descriptor blocking"); - } - }); - - std::vector buf(64 * 1024); - while (1) { - checkInterrupt(); - ssize_t rd = read(fd, buf.data(), buf.size()); - if (rd == -1) { - if (!block && (errno == EAGAIN || errno == EWOULDBLOCK)) - break; - if (errno != EINTR) - throw SysError("reading from file"); - } - else if (rd == 0) break; - else sink({reinterpret_cast(buf.data()), size_t(rd)}); - } -} - ////////////////////////////////////////////////////////////////////// -AutoCloseFD::AutoCloseFD() : fd{-1} {} + +AutoCloseFD::AutoCloseFD() : fd{INVALID_DESCRIPTOR} {} -AutoCloseFD::AutoCloseFD(int fd) : fd{fd} {} +AutoCloseFD::AutoCloseFD(Descriptor fd) : fd{fd} {} AutoCloseFD::AutoCloseFD(AutoCloseFD && that) : fd{that.fd} { - that.fd = -1; + that.fd = INVALID_DESCRIPTOR; } @@ -136,7 +44,7 @@ AutoCloseFD & AutoCloseFD::operator =(AutoCloseFD && that) { close(); fd = that.fd; - that.fd = -1; + that.fd = INVALID_DESCRIPTOR; return *this; } @@ -151,7 +59,7 @@ AutoCloseFD::~AutoCloseFD() } -int AutoCloseFD::get() const +Descriptor AutoCloseFD::get() const { return fd; } @@ -159,56 +67,46 @@ int AutoCloseFD::get() const void AutoCloseFD::close() { - if (fd != -1) { - if (::close(fd) == -1) + if (fd != INVALID_DESCRIPTOR) { + if(::close(fd) == -1) /* This should never happen. */ throw SysError("closing file descriptor %1%", fd); - fd = -1; + fd = INVALID_DESCRIPTOR; } } void AutoCloseFD::fsync() { - if (fd != -1) { - int result; + if (fd != INVALID_DESCRIPTOR) { + int result; + result = #if __APPLE__ - result = ::fcntl(fd, F_FULLFSYNC); + ::fcntl(fd, F_FULLFSYNC) #else - result = ::fsync(fd); + ::fsync(fd) #endif - if (result == -1) - throw SysError("fsync file descriptor %1%", fd); - } + ; + if (result == -1) + throw SysError("fsync file descriptor %1%", fd); + } } AutoCloseFD::operator bool() const { - return fd != -1; + return fd != INVALID_DESCRIPTOR; } -int AutoCloseFD::release() +Descriptor AutoCloseFD::release() { - int oldFD = fd; - fd = -1; + Descriptor oldFD = fd; + fd = INVALID_DESCRIPTOR; return oldFD; } -void Pipe::create() -{ - int fds[2]; -#if HAVE_PIPE2 - if (pipe2(fds, O_CLOEXEC) != 0) throw SysError("creating pipe"); -#else - if (pipe(fds) != 0) throw SysError("creating pipe"); - closeOnExec(fds[0]); - closeOnExec(fds[1]); -#endif - readSide = fds[0]; - writeSide = fds[1]; -} +////////////////////////////////////////////////////////////////////// void Pipe::close() @@ -217,38 +115,4 @@ void Pipe::close() writeSide.close(); } -////////////////////////////////////////////////////////////////////// - -void closeMostFDs(const std::set & exceptions) -{ -#if __linux__ - try { - for (auto & s : readDirectory("/proc/self/fd")) { - auto fd = std::stoi(s.name); - if (!exceptions.count(fd)) { - debug("closing leaked FD %d", fd); - close(fd); - } - } - return; - } catch (SystemError &) { - } -#endif - - int maxFD = 0; - maxFD = sysconf(_SC_OPEN_MAX); - for (int fd = 0; fd < maxFD; ++fd) - if (!exceptions.count(fd)) - close(fd); /* ignore result */ -} - - -void closeOnExec(int fd) -{ - int prev; - if ((prev = fcntl(fd, F_GETFD, 0)) == -1 || - fcntl(fd, F_SETFD, prev | FD_CLOEXEC) == -1) - throw SysError("setting close-on-exec flag"); -} - } diff --git a/src/libutil/file-descriptor.hh b/src/libutil/file-descriptor.hh index 80ec86135..719e1e444 100644 --- a/src/libutil/file-descriptor.hh +++ b/src/libutil/file-descriptor.hh @@ -9,53 +9,85 @@ namespace nix { struct Sink; struct Source; +/** + * Operating System capability + */ +typedef int Descriptor; + +const Descriptor INVALID_DESCRIPTOR = -1; + +/** + * Convert a native `Descriptor` to a POSIX file descriptor + * + * This is a no-op except on Windows. + */ +static inline Descriptor toDescriptor(int fd) +{ + return fd; +} + +/** + * Convert a POSIX file descriptor to a native `Descriptor` + * + * This is a no-op except on Windows. + */ +static inline int fromDescriptor(Descriptor fd, int flags) +{ + return fd; +} + /** * Read the contents of a resource into a string. */ -std::string readFile(int fd); +std::string readFile(Descriptor fd); /** * Wrappers arount read()/write() that read/write exactly the * requested number of bytes. */ -void readFull(int fd, char * buf, size_t count); +void readFull(Descriptor fd, char * buf, size_t count); -void writeFull(int fd, std::string_view s, bool allowInterrupts = true); +void writeFull(Descriptor fd, std::string_view s, bool allowInterrupts = true); /** * Read a line from a file descriptor. */ -std::string readLine(int fd); +std::string readLine(Descriptor fd); /** * Write a line to a file descriptor. */ -void writeLine(int fd, std::string s); +void writeLine(Descriptor fd, std::string s); /** * Read a file descriptor until EOF occurs. */ -std::string drainFD(int fd, bool block = true, const size_t reserveSize=0); +std::string drainFD(Descriptor fd, bool block = true, const size_t reserveSize=0); -void drainFD(int fd, Sink & sink, bool block = true); +void drainFD(Descriptor fd, Sink & sink, bool block = true); + +[[gnu::always_inline]] +inline Descriptor getStandardOut() { + return STDOUT_FILENO; +} /** * Automatic cleanup of resources. */ class AutoCloseFD { - int fd; + Descriptor fd; public: AutoCloseFD(); - AutoCloseFD(int fd); + AutoCloseFD(Descriptor fd); AutoCloseFD(const AutoCloseFD & fd) = delete; AutoCloseFD(AutoCloseFD&& fd); ~AutoCloseFD(); AutoCloseFD& operator =(const AutoCloseFD & fd) = delete; AutoCloseFD& operator =(AutoCloseFD&& fd); - int get() const; + Descriptor get() const; explicit operator bool() const; - int release(); + Descriptor release(); void close(); void fsync(); }; @@ -72,12 +104,12 @@ public: * Close all file descriptors except those listed in the given set. * Good practice in child processes. */ -void closeMostFDs(const std::set & exceptions); +void closeMostFDs(const std::set & exceptions); /** * Set the close-on-exec flag for the given file descriptor. */ -void closeOnExec(int fd); +void closeOnExec(Descriptor fd); MakeError(EndOfFile, Error); diff --git a/src/libutil/file-system.cc b/src/libutil/file-system.cc index 9f81ee452..89d309731 100644 --- a/src/libutil/file-system.cc +++ b/src/libutil/file-system.cc @@ -128,7 +128,7 @@ std::string_view baseNameOf(std::string_view path) return ""; auto last = path.size() - 1; - if (path[last] == '/' && last > 0) + while (last > 0 && path[last] == '/') last -= 1; auto pos = path.rfind('/', last); @@ -174,15 +174,23 @@ struct stat lstat(const Path & path) } +std::optional maybeLstat(const Path & path) +{ + std::optional st{std::in_place}; + if (lstat(path.c_str(), &*st)) + { + if (errno == ENOENT || errno == ENOTDIR) + st.reset(); + else + throw SysError("getting status of '%s'", path); + } + return st; +} + + bool pathExists(const Path & path) { - int res; - struct stat st; - res = lstat(path.c_str(), &st); - if (!res) return true; - if (errno != ENOENT && errno != ENOTDIR) - throw SysError("getting status of %1%", path); - return false; + return maybeLstat(path).has_value(); } bool pathAccessible(const Path & path) diff --git a/src/libutil/file-system.hh b/src/libutil/file-system.hh index 9d565c881..06a993829 100644 --- a/src/libutil/file-system.hh +++ b/src/libutil/file-system.hh @@ -84,6 +84,11 @@ bool isDirOrInDir(std::string_view path, std::string_view dir); */ struct stat stat(const Path & path); struct stat lstat(const Path & path); +/** + * `lstat` the given path if it exists. + * @return std::nullopt if the path doesn't exist, or an optional containing the result of `lstat` otherwise + */ +std::optional maybeLstat(const Path & path); /** * @return true iff the given path exists. diff --git a/src/libutil/fmt.hh b/src/libutil/fmt.hh index abbaf95b6..c178257d4 100644 --- a/src/libutil/fmt.hh +++ b/src/libutil/fmt.hh @@ -144,6 +144,10 @@ public: : HintFmt("%s", Uncolored(literal)) { } + static HintFmt fromFormatString(const std::string & format) { + return HintFmt(boost::format(format)); + } + /** * Interpolate the given arguments into the format string. */ diff --git a/src/libutil/json-utils.cc b/src/libutil/json-utils.cc index 61cef743d..7a7264a9a 100644 --- a/src/libutil/json-utils.cc +++ b/src/libutil/json-utils.cc @@ -1,5 +1,8 @@ #include "json-utils.hh" #include "error.hh" +#include "types.hh" +#include +#include namespace nix { @@ -18,26 +21,115 @@ nlohmann::json * get(nlohmann::json & map, const std::string & key) } const nlohmann::json & valueAt( - const nlohmann::json & map, + const nlohmann::json::object_t & map, const std::string & key) { if (!map.contains(key)) - throw Error("Expected JSON object to contain key '%s' but it doesn't", key); + throw Error("Expected JSON object to contain key '%s' but it doesn't: %s", key, nlohmann::json(map).dump()); - return map[key]; + return map.at(key); } -const nlohmann::json & ensureType( +std::optional optionalValueAt(const nlohmann::json & value, const std::string & key) +{ + try { + auto & v = valueAt(value, key); + return v.get(); + } catch (...) { + return std::nullopt; + } +} + + +std::optional getNullable(const nlohmann::json & value) +{ + if (value.is_null()) + return std::nullopt; + + return value.get(); +} + +/** + * Ensure the type of a JSON object is what you expect, failing with a + * ensure type if it isn't. + * + * Use before type conversions and element access to avoid ugly + * exceptions, but only part of this module to define the other `get*` + * functions. It is too cumbersome and easy to forget to expect regular + * JSON code to use it directly. + */ +static const nlohmann::json & ensureType( const nlohmann::json & value, nlohmann::json::value_type expectedType ) { if (value.type() != expectedType) throw Error( - "Expected JSON value to be of type '%s' but it is of type '%s'", + "Expected JSON value to be of type '%s' but it is of type '%s': %s", nlohmann::json(expectedType).type_name(), - value.type_name()); + value.type_name(), value.dump()); return value; } + +const nlohmann::json::object_t & getObject(const nlohmann::json & value) +{ + return ensureType(value, nlohmann::json::value_t::object).get_ref(); +} + +const nlohmann::json::array_t & getArray(const nlohmann::json & value) +{ + return ensureType(value, nlohmann::json::value_t::array).get_ref(); +} + +const nlohmann::json::string_t & getString(const nlohmann::json & value) +{ + return ensureType(value, nlohmann::json::value_t::string).get_ref(); +} + +const nlohmann::json::number_integer_t & getInteger(const nlohmann::json & value) +{ + return ensureType(value, nlohmann::json::value_t::number_integer).get_ref(); +} + +const nlohmann::json::boolean_t & getBoolean(const nlohmann::json & value) +{ + return ensureType(value, nlohmann::json::value_t::boolean).get_ref(); +} + +Strings getStringList(const nlohmann::json & value) +{ + auto & jsonArray = getArray(value); + + Strings stringList; + + for (const auto & elem : jsonArray) + stringList.push_back(getString(elem)); + + return stringList; +} + +StringMap getStringMap(const nlohmann::json & value) +{ + auto & jsonObject = getObject(value); + + StringMap stringMap; + + for (const auto & [key, value] : jsonObject) + stringMap[getString(key)] = getString(value); + + return stringMap; +} + +StringSet getStringSet(const nlohmann::json & value) +{ + auto & jsonArray = getArray(value); + + StringSet stringSet; + + for (const auto & elem : jsonArray) + stringSet.insert(getString(elem)); + + return stringSet; +} } diff --git a/src/libutil/json-utils.hh b/src/libutil/json-utils.hh index 06dd80cf7..2024624f4 100644 --- a/src/libutil/json-utils.hh +++ b/src/libutil/json-utils.hh @@ -3,6 +3,9 @@ #include #include +#include + +#include "types.hh" namespace nix { @@ -11,26 +14,30 @@ const nlohmann::json * get(const nlohmann::json & map, const std::string & key); nlohmann::json * get(nlohmann::json & map, const std::string & key); /** - * Get the value of a json object at a key safely, failing - * with a Nix Error if the key does not exist. + * Get the value of a json object at a key safely, failing with a nice + * error if the key does not exist. * * Use instead of nlohmann::json::at() to avoid ugly exceptions. - * - * _Does not check whether `map` is an object_, use `ensureType` for that. */ const nlohmann::json & valueAt( - const nlohmann::json & map, + const nlohmann::json::object_t & map, const std::string & key); +std::optional optionalValueAt(const nlohmann::json & value, const std::string & key); + /** - * Ensure the type of a json object is what you expect, failing - * with a Nix Error if it isn't. - * - * Use before type conversions and element access to avoid ugly exceptions. + * Downcast the json object, failing with a nice error if the conversion fails. + * See https://json.nlohmann.me/features/types/ */ -const nlohmann::json & ensureType( - const nlohmann::json & value, - nlohmann::json::value_type expectedType); +std::optional getNullable(const nlohmann::json & value); +const nlohmann::json::object_t & getObject(const nlohmann::json & value); +const nlohmann::json::array_t & getArray(const nlohmann::json & value); +const nlohmann::json::string_t & getString(const nlohmann::json & value); +const nlohmann::json::number_integer_t & getInteger(const nlohmann::json & value); +const nlohmann::json::boolean_t & getBoolean(const nlohmann::json & value); +Strings getStringList(const nlohmann::json & value); +StringMap getStringMap(const nlohmann::json & value); +StringSet getStringSet(const nlohmann::json & value); /** * For `adl_serializer>` below, we need to track what diff --git a/src/libutil/namespaces.cc b/src/libutil/linux/namespaces.cc similarity index 95% rename from src/libutil/namespaces.cc rename to src/libutil/linux/namespaces.cc index a789b321e..f8289ef39 100644 --- a/src/libutil/namespaces.cc +++ b/src/libutil/linux/namespaces.cc @@ -5,18 +5,14 @@ #include "processes.hh" #include "signals.hh" -#if __linux__ -# include -# include -# include "cgroup.hh" -#endif +#include +#include +#include "cgroup.hh" #include namespace nix { -#if __linux__ - bool userNamespacesSupported() { static auto res = [&]() -> bool @@ -101,19 +97,14 @@ bool mountAndPidNamespacesSupported() return res; } -#endif - ////////////////////////////////////////////////////////////////////// -#if __linux__ static AutoCloseFD fdSavedMountNamespace; static AutoCloseFD fdSavedRoot; -#endif void saveMountNamespace() { -#if __linux__ static std::once_flag done; std::call_once(done, []() { fdSavedMountNamespace = open("/proc/self/ns/mnt", O_RDONLY); @@ -122,12 +113,10 @@ void saveMountNamespace() fdSavedRoot = open("/proc/self/root", O_RDONLY); }); -#endif } void restoreMountNamespace() { -#if __linux__ try { auto savedCwd = absPath("."); @@ -146,15 +135,12 @@ void restoreMountNamespace() } catch (Error & e) { debug(e.msg()); } -#endif } void unshareFilesystem() { -#ifdef __linux__ if (unshare(CLONE_FS) != 0 && errno != EPERM) throw SysError("unsharing filesystem state in download thread"); -#endif } } diff --git a/src/libutil/namespaces.hh b/src/libutil/linux/namespaces.hh similarity index 96% rename from src/libutil/namespaces.hh rename to src/libutil/linux/namespaces.hh index 7e4e921a8..ef3c9123f 100644 --- a/src/libutil/namespaces.hh +++ b/src/libutil/linux/namespaces.hh @@ -26,12 +26,8 @@ void restoreMountNamespace(); */ void unshareFilesystem(); -#if __linux__ - bool userNamespacesSupported(); bool mountAndPidNamespacesSupported(); -#endif - } diff --git a/src/libutil/local.mk b/src/libutil/local.mk index 200026c1e..9773ef64f 100644 --- a/src/libutil/local.mk +++ b/src/libutil/local.mk @@ -5,8 +5,23 @@ libutil_NAME = libnixutil libutil_DIR := $(d) libutil_SOURCES := $(wildcard $(d)/*.cc $(d)/signature/*.cc) +ifdef HOST_UNIX + libutil_SOURCES += $(wildcard $(d)/unix/*.cc) +endif +ifdef HOST_LINUX + libutil_SOURCES += $(wildcard $(d)/linux/*.cc) +endif -libutil_CXXFLAGS += -I src/libutil +# Not just for this library itself, but also for downstream libraries using this library + +INCLUDE_libutil := -I $(d) +ifdef HOST_UNIX + INCLUDE_libutil += -I $(d)/unix +endif +ifdef HOST_LINUX + INCLUDE_libutil += -I $(d)/linux +endif +libutil_CXXFLAGS += $(INCLUDE_libutil) libutil_LDFLAGS += $(THREAD_LDFLAGS) $(LIBCURL_LIBS) $(SODIUM_LIBS) $(OPENSSL_LIBS) $(LIBBROTLI_LIBS) $(LIBARCHIVE_LIBS) $(BOOST_LDFLAGS) -lboost_context diff --git a/src/libutil/logging.cc b/src/libutil/logging.cc index 89fbd194a..5024c6081 100644 --- a/src/libutil/logging.cc +++ b/src/libutil/logging.cc @@ -37,8 +37,9 @@ void Logger::warn(const std::string & msg) void Logger::writeToStdout(std::string_view s) { - writeFull(STDOUT_FILENO, s); - writeFull(STDOUT_FILENO, "\n"); + Descriptor standard_out = getStandardOut(); + writeFull(standard_out, s); + writeFull(standard_out, "\n"); } class SimpleLogger : public Logger @@ -52,7 +53,7 @@ public: : printBuildLogs(printBuildLogs) { systemd = getEnv("IN_SYSTEMD") == "1"; - tty = shouldANSI(); + tty = isTTY(); } bool isVerbose() override { diff --git a/src/libutil/posix-source-accessor.cc b/src/libutil/posix-source-accessor.cc index 41c2db59a..8039d4b80 100644 --- a/src/libutil/posix-source-accessor.cc +++ b/src/libutil/posix-source-accessor.cc @@ -97,13 +97,7 @@ std::optional PosixSourceAccessor::cachedLstat(const CanonPath & pa if (i != cache->end()) return i->second; } - std::optional st{std::in_place}; - if (::lstat(absPath.c_str(), &*st)) { - if (errno == ENOENT || errno == ENOTDIR) - st.reset(); - else - throw SysError("getting status of '%s'", showPath(path)); - } + auto st = nix::maybeLstat(absPath.c_str()); auto cache(_cache.lock()); if (cache->size() >= 16384) cache->clear(); diff --git a/src/libutil/serialise.hh b/src/libutil/serialise.hh index d9522566f..6249ddaf5 100644 --- a/src/libutil/serialise.hh +++ b/src/libutil/serialise.hh @@ -119,18 +119,18 @@ protected: */ struct FdSink : BufferedSink { - int fd; + Descriptor fd; size_t written = 0; - FdSink() : fd(-1) { } - FdSink(int fd) : fd(fd) { } + FdSink() : fd(INVALID_DESCRIPTOR) { } + FdSink(Descriptor fd) : fd(fd) { } FdSink(FdSink&&) = default; FdSink & operator=(FdSink && s) { flush(); fd = s.fd; - s.fd = -1; + s.fd = INVALID_DESCRIPTOR; written = s.written; return *this; } @@ -151,18 +151,18 @@ private: */ struct FdSource : BufferedSource { - int fd; + Descriptor fd; size_t read = 0; BackedStringView endOfFileError{"unexpected end-of-file"}; - FdSource() : fd(-1) { } - FdSource(int fd) : fd(fd) { } + FdSource() : fd(INVALID_DESCRIPTOR) { } + FdSource(Descriptor fd) : fd(fd) { } FdSource(FdSource &&) = default; FdSource & operator=(FdSource && s) { fd = s.fd; - s.fd = -1; + s.fd = INVALID_DESCRIPTOR; read = s.read; return *this; } diff --git a/src/libutil/signals.hh b/src/libutil/signals.hh index 7e8beff33..8bff345c3 100644 --- a/src/libutil/signals.hh +++ b/src/libutil/signals.hh @@ -4,72 +4,39 @@ #include "types.hh" #include "error.hh" #include "logging.hh" -#include "ansicolor.hh" -#include -#include -#include -#include -#include - -#include - -#include #include -#include -#include -#include namespace nix { /* User interruption. */ -extern std::atomic _isInterrupted; +/** + * @note Does nothing on Windows + */ +static inline void setInterrupted(bool isInterrupted); -extern thread_local std::function interruptCheck; +/** + * @note Does nothing on Windows + */ +static inline bool getInterrupted(); +/** + * @note Does nothing on Windows + */ void setInterruptThrown(); -void _interrupted(); - -void inline checkInterrupt() -{ - if (_isInterrupted || (interruptCheck && interruptCheck())) - _interrupted(); -} +/** + * @note Does nothing on Windows + */ +inline void checkInterrupt(); +/** + * @note Never will happen on Windows + */ MakeError(Interrupted, BaseError); -/** - * Start a thread that handles various signals. Also block those signals - * on the current thread (and thus any threads created by it). - * Saves the signal mask before changing the mask to block those signals. - * See saveSignalMask(). - */ -void startSignalHandlerThread(); - -/** - * Saves the signal mask, which is the signal mask that nix will restore - * before creating child processes. - * See setChildSignalMask() to set an arbitrary signal mask instead of the - * current mask. - */ -void saveSignalMask(); - -/** - * To use in a process that already called `startSignalHandlerThread()` - * or `saveSignalMask()` first. - */ -void restoreSignals(); - -/** - * Sets the signal mask. Like saveSignalMask() but for a signal set that doesn't - * necessarily match the current thread's mask. - * See saveSignalMask() to set the saved mask to the current mask. - */ -void setChildSignalMask(sigset_t *sigs); - struct InterruptCallback { virtual ~InterruptCallback() { }; @@ -78,27 +45,21 @@ struct InterruptCallback /** * Register a function that gets called on SIGINT (in a non-signal * context). + * + * @note Does nothing on Windows */ std::unique_ptr createInterruptCallback( std::function callback); -void triggerInterrupt(); - /** * A RAII class that causes the current thread to receive SIGUSR1 when * the signal handler thread receives SIGINT. That is, this allows * SIGINT to be multiplexed to multiple threads. + * + * @note Does nothing on Windows */ -struct ReceiveInterrupts -{ - pthread_t target; - std::unique_ptr callback; - - ReceiveInterrupts() - : target(pthread_self()) - , callback(createInterruptCallback([&]() { pthread_kill(target, SIGUSR1); })) - { } -}; - +struct ReceiveInterrupts; } + +#include "signals-impl.hh" diff --git a/src/libutil/tarfile.cc b/src/libutil/tarfile.cc index 3bb6694f8..6bb2bd2f3 100644 --- a/src/libutil/tarfile.cc +++ b/src/libutil/tarfile.cc @@ -1,18 +1,21 @@ #include #include +#include "finally.hh" #include "serialise.hh" #include "tarfile.hh" #include "file-system.hh" namespace nix { -static int callback_open(struct archive *, void * self) +namespace { + +int callback_open(struct archive *, void * self) { return ARCHIVE_OK; } -static ssize_t callback_read(struct archive * archive, void * _self, const void * * buffer) +ssize_t callback_read(struct archive * archive, void * _self, const void ** buffer) { auto self = (TarArchive *) _self; *buffer = self->buffer.data(); @@ -27,41 +30,71 @@ static ssize_t callback_read(struct archive * archive, void * _self, const void } } -static int callback_close(struct archive *, void * self) +int callback_close(struct archive *, void * self) { return ARCHIVE_OK; } -void TarArchive::check(int err, const std::string & reason) +void checkLibArchive(archive * archive, int err, const std::string & reason) { if (err == ARCHIVE_EOF) throw EndOfFile("reached end of archive"); else if (err != ARCHIVE_OK) - throw Error(reason, archive_error_string(this->archive)); + throw Error(reason, archive_error_string(archive)); } -TarArchive::TarArchive(Source & source, bool raw) : buffer(65536) +constexpr auto defaultBufferSize = std::size_t{65536}; +} + +void TarArchive::check(int err, const std::string & reason) { - this->archive = archive_read_new(); - this->source = &source; + checkLibArchive(archive, err, reason); +} + +/// @brief Get filter_code from its name. +/// +/// libarchive does not provide a convenience function like archive_write_add_filter_by_name but for reading. +/// Instead it's necessary to use this kludge to convert method -> code and +/// then use archive_read_support_filter_by_code. Arguably this is better than +/// hand-rolling the equivalent function that is better implemented in libarchive. +int getArchiveFilterCodeByName(const std::string & method) +{ + auto * ar = archive_write_new(); + auto cleanup = Finally{[&ar]() { checkLibArchive(ar, archive_write_close(ar), "failed to close archive: %s"); }}; + auto err = archive_write_add_filter_by_name(ar, method.c_str()); + checkLibArchive(ar, err, "failed to get libarchive filter by name: %s"); + auto code = archive_filter_code(ar, 0); + return code; +} + +TarArchive::TarArchive(Source & source, bool raw, std::optional compression_method) + : archive{archive_read_new()} + , source{&source} + , buffer(defaultBufferSize) +{ + if (!compression_method) { + archive_read_support_filter_all(archive); + } else { + archive_read_support_filter_by_code(archive, getArchiveFilterCodeByName(*compression_method)); + } if (!raw) { - archive_read_support_filter_all(archive); archive_read_support_format_all(archive); } else { - archive_read_support_filter_all(archive); archive_read_support_format_raw(archive); archive_read_support_format_empty(archive); } + archive_read_set_option(archive, NULL, "mac-ext", NULL); - check(archive_read_open(archive, (void *)this, callback_open, callback_read, callback_close), "Failed to open archive (%s)"); + check( + archive_read_open(archive, (void *) this, callback_open, callback_read, callback_close), + "Failed to open archive (%s)"); } - TarArchive::TarArchive(const Path & path) + : archive{archive_read_new()} + , buffer(defaultBufferSize) { - this->archive = archive_read_new(); - archive_read_support_filter_all(archive); archive_read_support_format_all(archive); archive_read_set_option(archive, NULL, "mac-ext", NULL); @@ -75,19 +108,19 @@ void TarArchive::close() TarArchive::~TarArchive() { - if (this->archive) archive_read_free(this->archive); + if (this->archive) + archive_read_free(this->archive); } static void extract_archive(TarArchive & archive, const Path & destDir) { - int flags = ARCHIVE_EXTRACT_TIME - | ARCHIVE_EXTRACT_SECURE_SYMLINKS - | ARCHIVE_EXTRACT_SECURE_NODOTDOT; + int flags = ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_SECURE_SYMLINKS | ARCHIVE_EXTRACT_SECURE_NODOTDOT; for (;;) { struct archive_entry * entry; int r = archive_read_next_header(archive.archive, &entry); - if (r == ARCHIVE_EOF) break; + if (r == ARCHIVE_EOF) + break; auto name = archive_entry_pathname(entry); if (!name) throw Error("cannot get archive member name: %s", archive_error_string(archive.archive)); @@ -96,18 +129,16 @@ static void extract_archive(TarArchive & archive, const Path & destDir) else archive.check(r); - archive_entry_copy_pathname(entry, - (destDir + "/" + name).c_str()); + archive_entry_copy_pathname(entry, (destDir + "/" + name).c_str()); // sources can and do contain dirs with no rx bits if (archive_entry_filetype(entry) == AE_IFDIR && (archive_entry_mode(entry) & 0500) != 0500) archive_entry_set_mode(entry, archive_entry_mode(entry) | 0500); // Patch hardlink path - const char *original_hardlink = archive_entry_hardlink(entry); + const char * original_hardlink = archive_entry_hardlink(entry); if (original_hardlink) { - archive_entry_copy_hardlink(entry, - (destDir + "/" + original_hardlink).c_str()); + archive_entry_copy_hardlink(entry, (destDir + "/" + original_hardlink).c_str()); } archive.check(archive_read_extract(archive.archive, entry, flags)); @@ -140,7 +171,8 @@ time_t unpackTarfileToSink(TarArchive & archive, FileSystemObjectSink & parseSin // FIXME: merge with extract_archive struct archive_entry * entry; int r = archive_read_next_header(archive.archive, &entry); - if (r == ARCHIVE_EOF) break; + if (r == ARCHIVE_EOF) + break; auto path = archive_entry_pathname(entry); if (!path) throw Error("cannot get archive member name: %s", archive_error_string(archive.archive)); @@ -167,8 +199,9 @@ time_t unpackTarfileToSink(TarArchive & archive, FileSystemObjectSink & parseSin auto n = archive_read_data(archive.archive, buf.data(), buf.size()); if (n < 0) throw Error("cannot read file '%s' from tarball", path); - if (n == 0) break; - crf(std::string_view { + if (n == 0) + break; + crf(std::string_view{ (const char *) buf.data(), (size_t) n, }); diff --git a/src/libutil/tarfile.hh b/src/libutil/tarfile.hh index 6a9c42149..705d211e4 100644 --- a/src/libutil/tarfile.hh +++ b/src/libutil/tarfile.hh @@ -7,25 +7,36 @@ namespace nix { -struct TarArchive { +struct TarArchive +{ struct archive * archive; Source * source; std::vector buffer; void check(int err, const std::string & reason = "failed to extract archive (%s)"); - TarArchive(Source & source, bool raw = false); + explicit TarArchive(const Path & path); - TarArchive(const Path & path); + /// @brief Create a generic archive from source. + /// @param source - Input byte stream. + /// @param raw - Whether to enable raw file support. For more info look in docs: + /// https://manpages.debian.org/stretch/libarchive-dev/archive_read_format.3.en.html + /// @param compression_method - Primary compression method to use. std::nullopt means 'all'. + TarArchive(Source & source, bool raw = false, std::optional compression_method = std::nullopt); - /// disable copy constructor + /// Disable copy constructor. Explicitly default move assignment/constructor. TarArchive(const TarArchive &) = delete; + TarArchive & operator=(const TarArchive &) = delete; + TarArchive(TarArchive &&) = default; + TarArchive & operator=(TarArchive &&) = default; void close(); ~TarArchive(); }; +int getArchiveFilterCodeByName(const std::string & method); + void unpackTarfile(Source & source, const Path & destDir); void unpackTarfile(const Path & tarFile, const Path & destDir); diff --git a/src/libutil/terminal.cc b/src/libutil/terminal.cc index 8febc8771..096252f03 100644 --- a/src/libutil/terminal.cc +++ b/src/libutil/terminal.cc @@ -7,11 +7,14 @@ namespace nix { -bool shouldANSI() +bool isTTY() { - return isatty(STDERR_FILENO) + static const bool tty = + isatty(STDERR_FILENO) && getEnv("TERM").value_or("dumb") != "dumb" && !(getEnv("NO_COLOR").has_value() || getEnv("NOCOLOR").has_value()); + + return tty; } std::string filterANSIEscapes(std::string_view s, bool filterAll, unsigned int width) diff --git a/src/libutil/terminal.hh b/src/libutil/terminal.hh index 9cb191308..9d8d0c743 100644 --- a/src/libutil/terminal.hh +++ b/src/libutil/terminal.hh @@ -8,7 +8,7 @@ namespace nix { * Determine whether ANSI escape sequences are appropriate for the * present output. */ -bool shouldANSI(); +bool isTTY(); /** * Truncate a string to 'width' printable characters. If 'filterAll' diff --git a/src/libutil/thread-pool.cc b/src/libutil/thread-pool.cc index 9a7dfee56..805f31d80 100644 --- a/src/libutil/thread-pool.cc +++ b/src/libutil/thread-pool.cc @@ -82,7 +82,7 @@ void ThreadPool::doWork(bool mainThread) ReceiveInterrupts receiveInterrupts; if (!mainThread) - interruptCheck = [&]() { return (bool) quit; }; + unix::interruptCheck = [&]() { return (bool) quit; }; bool didWork = false; std::exception_ptr exc; diff --git a/src/libutil/unix/environment-variables.cc b/src/libutil/unix/environment-variables.cc new file mode 100644 index 000000000..9c6fd3b18 --- /dev/null +++ b/src/libutil/unix/environment-variables.cc @@ -0,0 +1,12 @@ +#include + +#include "environment-variables.hh" + +namespace nix { + +int setEnv(const char * name, const char * value) +{ + return ::setenv(name, value, 1); +} + +} diff --git a/src/libutil/unix/file-descriptor.cc b/src/libutil/unix/file-descriptor.cc new file mode 100644 index 000000000..27c8d821b --- /dev/null +++ b/src/libutil/unix/file-descriptor.cc @@ -0,0 +1,155 @@ +#include "file-system.hh" +#include "signals.hh" +#include "finally.hh" +#include "serialise.hh" + +#include +#include + +namespace nix { + +std::string readFile(int fd) +{ + struct stat st; + if (fstat(fd, &st) == -1) + throw SysError("statting file"); + + return drainFD(fd, true, st.st_size); +} + + +void readFull(int fd, char * buf, size_t count) +{ + while (count) { + checkInterrupt(); + ssize_t res = read(fd, buf, count); + if (res == -1) { + if (errno == EINTR) continue; + throw SysError("reading from file"); + } + if (res == 0) throw EndOfFile("unexpected end-of-file"); + count -= res; + buf += res; + } +} + + +void writeFull(int fd, std::string_view s, bool allowInterrupts) +{ + while (!s.empty()) { + if (allowInterrupts) checkInterrupt(); + ssize_t res = write(fd, s.data(), s.size()); + if (res == -1 && errno != EINTR) + throw SysError("writing to file"); + if (res > 0) + s.remove_prefix(res); + } +} + + +std::string readLine(int fd) +{ + std::string s; + while (1) { + checkInterrupt(); + char ch; + // FIXME: inefficient + ssize_t rd = read(fd, &ch, 1); + if (rd == -1) { + if (errno != EINTR) + throw SysError("reading a line"); + } else if (rd == 0) + throw EndOfFile("unexpected EOF reading a line"); + else { + if (ch == '\n') return s; + s += ch; + } + } +} + + +void drainFD(int fd, Sink & sink, bool block) +{ + // silence GCC maybe-uninitialized warning in finally + int saved = 0; + + if (!block) { + saved = fcntl(fd, F_GETFL); + if (fcntl(fd, F_SETFL, saved | O_NONBLOCK) == -1) + throw SysError("making file descriptor non-blocking"); + } + + Finally finally([&]() { + if (!block) { + if (fcntl(fd, F_SETFL, saved) == -1) + throw SysError("making file descriptor blocking"); + } + }); + + std::vector buf(64 * 1024); + while (1) { + checkInterrupt(); + ssize_t rd = read(fd, buf.data(), buf.size()); + if (rd == -1) { + if (!block && (errno == EAGAIN || errno == EWOULDBLOCK)) + break; + if (errno != EINTR) + throw SysError("reading from file"); + } + else if (rd == 0) break; + else sink({reinterpret_cast(buf.data()), (size_t) rd}); + } +} + +////////////////////////////////////////////////////////////////////// + +void Pipe::create() +{ + int fds[2]; +#if HAVE_PIPE2 + if (pipe2(fds, O_CLOEXEC) != 0) throw SysError("creating pipe"); +#else + if (pipe(fds) != 0) throw SysError("creating pipe"); + closeOnExec(fds[0]); + closeOnExec(fds[1]); +#endif + readSide = fds[0]; + writeSide = fds[1]; +} + + +////////////////////////////////////////////////////////////////////// + +void closeMostFDs(const std::set & exceptions) +{ +#if __linux__ + try { + for (auto & s : readDirectory("/proc/self/fd")) { + auto fd = std::stoi(s.name); + if (!exceptions.count(fd)) { + debug("closing leaked FD %d", fd); + close(fd); + } + } + return; + } catch (SysError &) { + } +#endif + + int maxFD = 0; + maxFD = sysconf(_SC_OPEN_MAX); + for (int fd = 0; fd < maxFD; ++fd) + if (!exceptions.count(fd)) + close(fd); /* ignore result */ +} + + +void closeOnExec(int fd) +{ + int prev; + if ((prev = fcntl(fd, F_GETFD, 0)) == -1 || + fcntl(fd, F_SETFD, prev | FD_CLOEXEC) == -1) + throw SysError("setting close-on-exec flag"); +} + +} diff --git a/src/libutil/monitor-fd.hh b/src/libutil/unix/monitor-fd.hh similarity index 97% rename from src/libutil/monitor-fd.hh rename to src/libutil/unix/monitor-fd.hh index 228fb13f8..103894de9 100644 --- a/src/libutil/monitor-fd.hh +++ b/src/libutil/unix/monitor-fd.hh @@ -50,7 +50,7 @@ public: */ if (count == 0) continue; if (fds[0].revents & POLLHUP) { - triggerInterrupt(); + unix::triggerInterrupt(); break; } /* This will only happen on macOS. We sleep a bit to diff --git a/src/libutil/processes.cc b/src/libutil/unix/processes.cc similarity index 100% rename from src/libutil/processes.cc rename to src/libutil/unix/processes.cc diff --git a/src/libutil/processes.hh b/src/libutil/unix/processes.hh similarity index 100% rename from src/libutil/processes.hh rename to src/libutil/unix/processes.hh diff --git a/src/libutil/unix/signals-impl.hh b/src/libutil/unix/signals-impl.hh new file mode 100644 index 000000000..7ac8c914d --- /dev/null +++ b/src/libutil/unix/signals-impl.hh @@ -0,0 +1,111 @@ +#pragma once +/** + * @file + * + * Implementation of some inline definitions for Unix signals, and also + * some extra Unix-only interfaces. + * + * (The only reason everything about signals isn't Unix-only is some + * no-op definitions are provided on Windows to avoid excess CPP in + * downstream code.) + */ + +#include "types.hh" +#include "error.hh" +#include "logging.hh" +#include "ansicolor.hh" + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +namespace nix { + +/* User interruption. */ + +namespace unix { + +extern std::atomic _isInterrupted; + +extern thread_local std::function interruptCheck; + +void _interrupted(); + +/** + * Sets the signal mask. Like saveSignalMask() but for a signal set that doesn't + * necessarily match the current thread's mask. + * See saveSignalMask() to set the saved mask to the current mask. + */ +void setChildSignalMask(sigset_t *sigs); + +/** + * Start a thread that handles various signals. Also block those signals + * on the current thread (and thus any threads created by it). + * Saves the signal mask before changing the mask to block those signals. + * See saveSignalMask(). + */ +void startSignalHandlerThread(); + +/** + * Saves the signal mask, which is the signal mask that nix will restore + * before creating child processes. + * See setChildSignalMask() to set an arbitrary signal mask instead of the + * current mask. + */ +void saveSignalMask(); + +/** + * To use in a process that already called `startSignalHandlerThread()` + * or `saveSignalMask()` first. + */ +void restoreSignals(); + +void triggerInterrupt(); + +} + +static inline void setInterrupted(bool isInterrupted) +{ + unix::_isInterrupted = isInterrupted; +} + +static inline bool getInterrupted() +{ + return unix::_isInterrupted; +} + +void inline checkInterrupt() +{ + using namespace unix; + if (_isInterrupted || (interruptCheck && interruptCheck())) + _interrupted(); +} + +/** + * A RAII class that causes the current thread to receive SIGUSR1 when + * the signal handler thread receives SIGINT. That is, this allows + * SIGINT to be multiplexed to multiple threads. + */ +struct ReceiveInterrupts +{ + pthread_t target; + std::unique_ptr callback; + + ReceiveInterrupts() + : target(pthread_self()) + , callback(createInterruptCallback([&]() { pthread_kill(target, SIGUSR1); })) + { } +}; + + +} diff --git a/src/libutil/signals.cc b/src/libutil/unix/signals.cc similarity index 92% rename from src/libutil/signals.cc rename to src/libutil/unix/signals.cc index eaa4ea30e..7e30687d8 100644 --- a/src/libutil/signals.cc +++ b/src/libutil/unix/signals.cc @@ -8,17 +8,22 @@ namespace nix { -std::atomic _isInterrupted = false; +using namespace unix; +std::atomic unix::_isInterrupted = false; + +namespace unix { static thread_local bool interruptThrown = false; -thread_local std::function interruptCheck; +} + +thread_local std::function unix::interruptCheck; void setInterruptThrown() { - interruptThrown = true; + unix::interruptThrown = true; } -void _interrupted() +void unix::_interrupted() { /* Block user interrupts while an exception is being handled. Throwing an exception while another exception is being handled @@ -65,7 +70,7 @@ static void signalHandlerThread(sigset_t set) } } -void triggerInterrupt() +void unix::triggerInterrupt() { _isInterrupted = true; @@ -96,7 +101,7 @@ void triggerInterrupt() static sigset_t savedSignalMask; static bool savedSignalMaskIsSet = false; -void setChildSignalMask(sigset_t * sigs) +void unix::setChildSignalMask(sigset_t * sigs) { assert(sigs); // C style function, but think of sigs as a reference @@ -115,14 +120,14 @@ void setChildSignalMask(sigset_t * sigs) savedSignalMaskIsSet = true; } -void saveSignalMask() { +void unix::saveSignalMask() { if (sigprocmask(SIG_BLOCK, nullptr, &savedSignalMask)) throw SysError("querying signal mask"); savedSignalMaskIsSet = true; } -void startSignalHandlerThread() +void unix::startSignalHandlerThread() { updateWindowSize(); @@ -141,7 +146,7 @@ void startSignalHandlerThread() std::thread(signalHandlerThread, set).detach(); } -void restoreSignals() +void unix::restoreSignals() { // If startSignalHandlerThread wasn't called, that means we're not running // in a proper libmain process, but a process that presumably manages its diff --git a/src/libutil/unix-domain-socket.cc b/src/libutil/unix/unix-domain-socket.cc similarity index 100% rename from src/libutil/unix-domain-socket.cc rename to src/libutil/unix/unix-domain-socket.cc diff --git a/src/libutil/unix-domain-socket.hh b/src/libutil/unix/unix-domain-socket.hh similarity index 100% rename from src/libutil/unix-domain-socket.hh rename to src/libutil/unix/unix-domain-socket.hh diff --git a/src/libutil/unix/users.cc b/src/libutil/unix/users.cc new file mode 100644 index 000000000..58063a953 --- /dev/null +++ b/src/libutil/unix/users.cc @@ -0,0 +1,66 @@ +#include "util.hh" +#include "users.hh" +#include "environment-variables.hh" +#include "file-system.hh" + +#include +#include +#include + +namespace nix { + +std::string getUserName() +{ + auto pw = getpwuid(geteuid()); + std::string name = pw ? pw->pw_name : getEnv("USER").value_or(""); + if (name.empty()) + throw Error("cannot figure out user name"); + return name; +} + +Path getHomeOf(uid_t userId) +{ + std::vector buf(16384); + struct passwd pwbuf; + struct passwd * pw; + if (getpwuid_r(userId, &pwbuf, buf.data(), buf.size(), &pw) != 0 + || !pw || !pw->pw_dir || !pw->pw_dir[0]) + throw Error("cannot determine user's home directory"); + return pw->pw_dir; +} + +Path getHome() +{ + static Path homeDir = []() + { + std::optional unownedUserHomeDir = {}; + auto homeDir = getEnv("HOME"); + if (homeDir) { + // Only use $HOME if doesn't exist or is owned by the current user. + struct stat st; + int result = stat(homeDir->c_str(), &st); + if (result != 0) { + if (errno != ENOENT) { + warn("couldn't stat $HOME ('%s') for reason other than not existing ('%d'), falling back to the one defined in the 'passwd' file", *homeDir, errno); + homeDir.reset(); + } + } else if (st.st_uid != geteuid()) { + unownedUserHomeDir.swap(homeDir); + } + } + if (!homeDir) { + homeDir = getHomeOf(geteuid()); + if (unownedUserHomeDir.has_value() && unownedUserHomeDir != homeDir) { + warn("$HOME ('%s') is not owned by you, falling back to the one defined in the 'passwd' file ('%s')", *unownedUserHomeDir, *homeDir); + } + } + return *homeDir; + }(); + return homeDir; +} + +bool isRootUser() { + return getuid() == 0; +} + +} diff --git a/src/libutil/users.cc b/src/libutil/users.cc index 95a641322..d546e364f 100644 --- a/src/libutil/users.cc +++ b/src/libutil/users.cc @@ -3,63 +3,8 @@ #include "environment-variables.hh" #include "file-system.hh" -#include -#include -#include - namespace nix { -std::string getUserName() -{ - auto pw = getpwuid(geteuid()); - std::string name = pw ? pw->pw_name : getEnv("USER").value_or(""); - if (name.empty()) - throw Error("cannot figure out user name"); - return name; -} - -Path getHomeOf(uid_t userId) -{ - std::vector buf(16384); - struct passwd pwbuf; - struct passwd * pw; - if (getpwuid_r(userId, &pwbuf, buf.data(), buf.size(), &pw) != 0 - || !pw || !pw->pw_dir || !pw->pw_dir[0]) - throw Error("cannot determine user's home directory"); - return pw->pw_dir; -} - -Path getHome() -{ - static Path homeDir = []() - { - std::optional unownedUserHomeDir = {}; - auto homeDir = getEnv("HOME"); - if (homeDir) { - // Only use $HOME if doesn't exist or is owned by the current user. - struct stat st; - int result = stat(homeDir->c_str(), &st); - if (result != 0) { - if (errno != ENOENT) { - warn("couldn't stat $HOME ('%s') for reason other than not existing ('%d'), falling back to the one defined in the 'passwd' file", *homeDir, errno); - homeDir.reset(); - } - } else if (st.st_uid != geteuid()) { - unownedUserHomeDir.swap(homeDir); - } - } - if (!homeDir) { - homeDir = getHomeOf(geteuid()); - if (unownedUserHomeDir.has_value() && unownedUserHomeDir != homeDir) { - warn("$HOME ('%s') is not owned by you, falling back to the one defined in the 'passwd' file ('%s')", *unownedUserHomeDir, *homeDir); - } - } - return *homeDir; - }(); - return homeDir; -} - - Path getCacheDir() { auto cacheDir = getEnv("XDG_CACHE_HOME"); diff --git a/src/libutil/users.hh b/src/libutil/users.hh index cecbb8bfb..449e5bbe9 100644 --- a/src/libutil/users.hh +++ b/src/libutil/users.hh @@ -55,4 +55,10 @@ Path createNixStateDir(); */ std::string expandTilde(std::string_view path); + +/** + * Is the current user UID 0 on Unix? + */ +bool isRootUser(); + } diff --git a/src/libutil/util.cc b/src/libutil/util.cc index 06124bf15..103ce4232 100644 --- a/src/libutil/util.cc +++ b/src/libutil/util.cc @@ -4,7 +4,6 @@ #include #include #include -#include #include #include diff --git a/src/nix-build/nix-build.cc b/src/nix-build/nix-build.cc index 35eef5b83..60dea3a80 100644 --- a/src/nix-build/nix-build.cc +++ b/src/nix-build/nix-build.cc @@ -24,6 +24,7 @@ #include "common-eval-args.hh" #include "attr-path.hh" #include "legacy.hh" +#include "users.hh" using namespace nix; using namespace std::string_literals; @@ -287,7 +288,7 @@ static void main_nix_build(int argc, char * * argv) } if (runEnv) - setenv("IN_NIX_SHELL", pure ? "pure" : "impure", 1); + setEnv("IN_NIX_SHELL", pure ? "pure" : "impure"); PackageInfos drvs; @@ -572,8 +573,9 @@ static void main_nix_build(int argc, char * * argv) "BASH=%5%; " "set +e; " R"s([ -n "$PS1" -a -z "$NIX_SHELL_PRESERVE_PROMPT" ] && )s" + - (getuid() == 0 ? R"s(PS1='\n\[\033[1;31m\][nix-shell:\w]\$\[\033[0m\] '; )s" - : R"s(PS1='\n\[\033[1;32m\][nix-shell:\w]\$\[\033[0m\] '; )s") + + (isRootUser() + ? R"s(PS1='\n\[\033[1;31m\][nix-shell:\w]\$\[\033[0m\] '; )s" + : R"s(PS1='\n\[\033[1;32m\][nix-shell:\w]\$\[\033[0m\] '; )s") + "if [ \"$(type -t runHook)\" = function ]; then runHook shellHook; fi; " "unset NIX_ENFORCE_PURITY; " "shopt -u nullglob; " diff --git a/src/nix-env/nix-env.cc b/src/nix-env/nix-env.cc index f79755375..1cc33558f 100644 --- a/src/nix-env/nix-env.cc +++ b/src/nix-env/nix-env.cc @@ -16,6 +16,7 @@ #include "xml-writer.hh" #include "legacy.hh" #include "eval-settings.hh" // for defexpr +#include "terminal.hh" #include #include @@ -108,7 +109,7 @@ static void getAllExprs(EvalState & state, const SourcePath & path, StringSet & seen, BindingsBuilder & attrs) { StringSet namesSorted; - for (auto & [name, _] : path.readDirectory()) namesSorted.insert(name); + for (auto & [name, _] : path.resolveSymlinks().readDirectory()) namesSorted.insert(name); for (auto & i : namesSorted) { /* Ignore the manifest.nix used by profiles. This is @@ -1089,7 +1090,7 @@ static void opQuery(Globals & globals, Strings opFlags, Strings opArgs) return; } - bool tty = isatty(STDOUT_FILENO); + bool tty = isTTY(); RunPager pager; Table table; @@ -1413,7 +1414,7 @@ static int main_nix_env(int argc, char * * argv) replaceSymlink( defaultChannelsDir(), nixExprPath + "/channels"); - if (getuid() != 0) + if (!isRootUser()) replaceSymlink( rootChannelsDir(), nixExprPath + "/channels_root"); diff --git a/src/nix/develop.cc b/src/nix/develop.cc index c1842f2d5..bb96f7786 100644 --- a/src/nix/develop.cc +++ b/src/nix/develop.cc @@ -603,7 +603,7 @@ struct CmdDevelop : Common, MixEnvironment setEnviron(); // prevent garbage collection until shell exits - setenv("NIX_GCROOT", gcroot.c_str(), 1); + setEnv("NIX_GCROOT", gcroot.c_str()); Path shell = "bash"; @@ -648,7 +648,7 @@ struct CmdDevelop : Common, MixEnvironment // Override SHELL with the one chosen for this environment. // This is to make sure the system shell doesn't leak into the build environment. - setenv("SHELL", shell.c_str(), 1); + setEnv("SHELL", shell.c_str()); // If running a phase or single command, don't want an interactive shell running after // Ctrl-C, so don't pass --rcfile diff --git a/src/nix/local.mk b/src/nix/local.mk index 1d6f560d6..9f6f31b3a 100644 --- a/src/nix/local.mk +++ b/src/nix/local.mk @@ -12,9 +12,19 @@ nix_SOURCES := \ $(wildcard src/nix-daemon/*.cc) \ $(wildcard src/nix-env/*.cc) \ $(wildcard src/nix-instantiate/*.cc) \ - $(wildcard src/nix-store/*.cc) \ + $(wildcard src/nix-store/*.cc) -nix_CXXFLAGS += -I src/libutil -I src/libstore -I src/libfetchers -I src/libexpr -I src/libmain -I src/libcmd -I doc/manual +ifdef HOST_UNIX +nix_SOURCES += \ + $(wildcard $(d)/unix/*.cc) +endif + +INCLUDE_nix := -I $(d) +ifdef HOST_UNIX + INCLUDE_nix += -I $(d)/unix +endif + +nix_CXXFLAGS += $(INCLUDE_libutil) $(INCLUDE_libstore) $(INCLUDE_libfetchers) $(INCLUDE_libexpr) $(INCLUDE_libmain) -I src/libcmd -I doc/manual $(INCLUDE_nix) nix_LIBS = libexpr libmain libfetchers libstore libutil libcmd diff --git a/src/nix/main.cc b/src/nix/main.cc index 5af5f2e41..25f81e48b 100644 --- a/src/nix/main.cc +++ b/src/nix/main.cc @@ -2,7 +2,6 @@ #include "args/root.hh" #include "current-process.hh" -#include "namespaces.hh" #include "command.hh" #include "common-args.hh" #include "eval.hh" @@ -16,6 +15,7 @@ #include "loggers.hh" #include "markdown.hh" #include "memory-input-accessor.hh" +#include "terminal.hh" #include #include @@ -26,6 +26,10 @@ #include +#if __linux__ +# include "namespaces.hh" +#endif + extern std::string chrootHelperName; void chrootHelper(int argc, char * * argv); @@ -347,7 +351,7 @@ void mainWrapped(int argc, char * * argv) initGC(); #if __linux__ - if (getuid() == 0) { + if (isRootUser()) { try { saveMountNamespace(); if (unshare(CLONE_NEWNS) == -1) @@ -375,7 +379,9 @@ void mainWrapped(int argc, char * * argv) setLogFormat("bar"); settings.verboseBuild = false; - if (isatty(STDERR_FILENO)) { + + // If on a terminal, progress will be displayed via progress bars etc. (thus verbosity=notice) + if (nix::isTTY()) { verbosity = lvlNotice; } else { verbosity = lvlInfo; diff --git a/src/nix/prefetch.cc b/src/nix/prefetch.cc index fabec5d88..b64e6d899 100644 --- a/src/nix/prefetch.cc +++ b/src/nix/prefetch.cc @@ -11,6 +11,7 @@ #include "legacy.hh" #include "posix-source-accessor.hh" #include "misc-store-flags.hh" +#include "terminal.hh" #include @@ -188,7 +189,7 @@ static int main_nix_prefetch_url(int argc, char * * argv) Finally f([]() { stopProgressBar(); }); - if (isatty(STDERR_FILENO)) + if (isTTY()) startProgressBar(); auto store = openStore(); diff --git a/src/nix/repl.cc b/src/nix/repl.cc index 63fe3044b..8bbfe0f07 100644 --- a/src/nix/repl.cc +++ b/src/nix/repl.cc @@ -47,15 +47,6 @@ struct CmdRepl : RawInstallablesCommand void applyDefaultInstallables(std::vector & rawInstallables) override { - if (!experimentalFeatureSettings.isEnabled(Xp::Flakes) && !(file) && rawInstallables.size() >= 1) { - warn("future versions of Nix will require using `--file` to load a file"); - if (rawInstallables.size() > 1) - warn("more than one input file is not currently supported"); - auto filePath = rawInstallables[0].data(); - file = std::optional(filePath); - rawInstallables.front() = rawInstallables.back(); - rawInstallables.pop_back(); - } if (rawInstallables.empty() && (file.has_value() || expr.has_value())) { rawInstallables.push_back("."); } diff --git a/src/nix/daemon.cc b/src/nix/unix/daemon.cc similarity index 100% rename from src/nix/daemon.cc rename to src/nix/unix/daemon.cc diff --git a/src/nix/daemon.md b/src/nix/unix/daemon.md similarity index 100% rename from src/nix/daemon.md rename to src/nix/unix/daemon.md diff --git a/src/nix/fmt.cc b/src/nix/unix/fmt.cc similarity index 100% rename from src/nix/fmt.cc rename to src/nix/unix/fmt.cc diff --git a/src/nix/fmt.md b/src/nix/unix/fmt.md similarity index 100% rename from src/nix/fmt.md rename to src/nix/unix/fmt.md diff --git a/src/nix/run.cc b/src/nix/unix/run.cc similarity index 99% rename from src/nix/run.cc rename to src/nix/unix/run.cc index c4e0c19a2..d8394c616 100644 --- a/src/nix/run.cc +++ b/src/nix/unix/run.cc @@ -167,7 +167,7 @@ struct CmdShell : InstallablesCommand, MixEnvironment auto unixPath = tokenizeString(getEnv("PATH").value_or(""), ":"); unixPath.insert(unixPath.begin(), pathAdditions.begin(), pathAdditions.end()); auto unixPathString = concatStringsSep(":", unixPath); - setenv("PATH", unixPathString.c_str(), 1); + setEnv("PATH", unixPathString.c_str()); for (auto const& pathV : extraPathVarMapping) { setenv(pathV.first.c_str(), concatStringsSep(":", extraPathVars[pathV.first]).c_str(), 1); diff --git a/src/nix/run.hh b/src/nix/unix/run.hh similarity index 100% rename from src/nix/run.hh rename to src/nix/unix/run.hh diff --git a/src/nix/run.md b/src/nix/unix/run.md similarity index 100% rename from src/nix/run.md rename to src/nix/unix/run.md diff --git a/src/nix/upgrade-nix.cc b/src/nix/unix/upgrade-nix.cc similarity index 100% rename from src/nix/upgrade-nix.cc rename to src/nix/unix/upgrade-nix.cc diff --git a/src/nix/upgrade-nix.md b/src/nix/unix/upgrade-nix.md similarity index 100% rename from src/nix/upgrade-nix.md rename to src/nix/unix/upgrade-nix.md diff --git a/src/resolve-system-dependencies/local.mk b/src/resolve-system-dependencies/local.mk index fc48a8417..e138c4080 100644 --- a/src/resolve-system-dependencies/local.mk +++ b/src/resolve-system-dependencies/local.mk @@ -6,7 +6,7 @@ resolve-system-dependencies_DIR := $(d) resolve-system-dependencies_INSTALL_DIR := $(libexecdir)/nix -resolve-system-dependencies_CXXFLAGS += -I src/libutil -I src/libstore -I src/libmain +resolve-system-dependencies_CXXFLAGS += $(INCLUDE_libutil) $(INCLUDE_libstore) $(INCLUDE_libmain) resolve-system-dependencies_LIBS := libstore libmain libutil diff --git a/tests/functional/check.sh b/tests/functional/check.sh index e13abf747..38883c5d7 100644 --- a/tests/functional/check.sh +++ b/tests/functional/check.sh @@ -34,6 +34,21 @@ nix-build check.nix -A failed --argstr checkBuildId $checkBuildId \ [ "$status" = "100" ] if checkBuildTempDirRemoved $TEST_ROOT/log; then false; fi +test_custom_build_dir() { + local customBuildDir="$TEST_ROOT/custom-build-dir" + + # Nix does not create the parent directories, and perhaps it shouldn't try to + # decide the permissions of build-dir. + mkdir "$customBuildDir" + nix-build check.nix -A failed --argstr checkBuildId $checkBuildId \ + --no-out-link --keep-failed --option build-dir "$TEST_ROOT/custom-build-dir" 2> $TEST_ROOT/log || status=$? + [ "$status" = "100" ] + [[ 1 == "$(count "$customBuildDir/nix-build-"*)" ]] + local buildDir="$customBuildDir/nix-build-"* + grep $checkBuildId $buildDir/checkBuildId +} +test_custom_build_dir + nix-build check.nix -A deterministic --argstr checkBuildId $checkBuildId \ --no-out-link 2> $TEST_ROOT/log checkBuildTempDirRemoved $TEST_ROOT/log diff --git a/tests/functional/chroot-store.sh b/tests/functional/chroot-store.sh new file mode 100644 index 000000000..9e589d04b --- /dev/null +++ b/tests/functional/chroot-store.sh @@ -0,0 +1,45 @@ +source common.sh + +echo example > $TEST_ROOT/example.txt +mkdir -p $TEST_ROOT/x + +export NIX_STORE_DIR=/nix2/store + +CORRECT_PATH=$(cd $TEST_ROOT && nix-store --store ./x --add example.txt) + +[[ $CORRECT_PATH =~ ^/nix2/store/.*-example.txt$ ]] + +PATH1=$(cd $TEST_ROOT && nix path-info --store ./x $CORRECT_PATH) +[ $CORRECT_PATH == $PATH1 ] + +PATH2=$(nix path-info --store "$TEST_ROOT/x" $CORRECT_PATH) +[ $CORRECT_PATH == $PATH2 ] + +PATH3=$(nix path-info --store "local?root=$TEST_ROOT/x" $CORRECT_PATH) +[ $CORRECT_PATH == $PATH3 ] + +# Ensure store info trusted works with local store +nix --store $TEST_ROOT/x store info --json | jq -e '.trusted' + +# Test building in a chroot store. +if canUseSandbox; then + + flakeDir=$TEST_ROOT/flake + mkdir -p $flakeDir + + cat > $flakeDir/flake.nix < /dev/null } +# Return the number of arguments +count() { + echo $# +} + trap onError ERR fi # COMMON_VARS_AND_FUNCTIONS_SH_SOURCED diff --git a/tests/functional/config.sh b/tests/functional/config.sh index 324fe95bd..efdf2a958 100644 --- a/tests/functional/config.sh +++ b/tests/functional/config.sh @@ -43,6 +43,16 @@ export NIX_USER_CONF_FILES=$here/config/nix-with-substituters.conf var=$(nix config show | grep '^substituters =' | cut -d '=' -f 2 | xargs) [[ $var == https://example.com ]] +# Test that we can include a file. +export NIX_USER_CONF_FILES=$here/config/nix-with-include.conf +var=$(nix config show | grep '^allowed-uris =' | cut -d '=' -f 2 | xargs) +[[ $var == https://github.com/NixOS/nix ]] + +# Test that we can !include a file. +export NIX_USER_CONF_FILES=$here/config/nix-with-bang-include.conf +var=$(nix config show | grep '^experimental-features =' | cut -d '=' -f 2 | xargs) +[[ $var == nix-command ]] + # Test that it's possible to load config from the environment prev=$(nix config show | grep '^cores' | cut -d '=' -f 2 | xargs) export NIX_CONFIG="cores = 4242"$'\n'"experimental-features = nix-command flakes" @@ -56,4 +66,4 @@ exp_features=$(nix config show | grep '^experimental-features' | cut -d '=' -f 2 # Test that it's possible to retrieve a single setting's value val=$(nix config show | grep '^warn-dirty' | cut -d '=' -f 2 | xargs) val2=$(nix config show warn-dirty) -[[ $val == $val2 ]] +[[ $val == $val2 ]] \ No newline at end of file diff --git a/tests/functional/config/extra-config.conf b/tests/functional/config/extra-config.conf new file mode 100644 index 000000000..d110f06e4 --- /dev/null +++ b/tests/functional/config/extra-config.conf @@ -0,0 +1 @@ +allowed-uris = https://github.com/NixOS/nix \ No newline at end of file diff --git a/tests/functional/config/nix-with-bang-include.conf b/tests/functional/config/nix-with-bang-include.conf new file mode 100644 index 000000000..fa600e6ff --- /dev/null +++ b/tests/functional/config/nix-with-bang-include.conf @@ -0,0 +1,2 @@ +experimental-features = nix-command +!include ./missing-extra-config.conf \ No newline at end of file diff --git a/tests/functional/config/nix-with-include.conf b/tests/functional/config/nix-with-include.conf new file mode 100644 index 000000000..17b8958ba --- /dev/null +++ b/tests/functional/config/nix-with-include.conf @@ -0,0 +1,2 @@ +experimental-features = nix-command +include ./extra-config.conf \ No newline at end of file diff --git a/tests/functional/flakes/build-paths.sh b/tests/functional/flakes/build-paths.sh index ff012e1b3..4e5c68095 100644 --- a/tests/functional/flakes/build-paths.sh +++ b/tests/functional/flakes/build-paths.sh @@ -56,6 +56,23 @@ cat > $flake1Dir/flake.nix < \$foo/file + echo "out" > \$out/file + ''; + }; + in top // { + foo = top.foo // { + outputSpecified = true; + }; + }; }; } EOF @@ -94,3 +111,10 @@ nix build --json --out-link $TEST_ROOT/result $flake1Dir#a12 expectStderr 1 nix build --impure --json --out-link $TEST_ROOT/result $flake1Dir#a13 \ | grepQuiet "has 2 entries in its context. It should only have exactly one entry" + +# Test accessing output in installables with `.` (foobarbaz.) +nix build --json --no-link $flake1Dir#a14.foo | jq --exit-status ' + (.[0] | + (.drvPath | match(".*dot-installable.drv")) and + (.outputs | keys == ["foo"])) +' diff --git a/tests/functional/flakes/flakes.sh b/tests/functional/flakes/flakes.sh index cbbc69dc9..8d0579112 100644 --- a/tests/functional/flakes/flakes.sh +++ b/tests/functional/flakes/flakes.sh @@ -93,6 +93,24 @@ foo EOF chmod +x $nonFlakeDir/shebang-comments.sh +cat > $nonFlakeDir/shebang-different-comments.sh < $nonFlakeDir/shebang-reject.sh <&1 | grepQuiet -F 'error: unsupported unquoted character in nix shebang: *. Use double backticks to escape?' diff --git a/tests/functional/lang.sh b/tests/functional/lang.sh index 12df32c87..e35795a7a 100755 --- a/tests/functional/lang.sh +++ b/tests/functional/lang.sh @@ -68,8 +68,16 @@ done for i in lang/eval-fail-*.nix; do echo "evaluating $i (should fail)"; i=$(basename "$i" .nix) + flags="$( + if [[ -e "lang/$i.flags" ]]; then + sed -e 's/#.*//' < "lang/$i.flags" + else + # note that show-trace is also set by init.sh + echo "--eval --strict --show-trace" + fi + )" if - expectStderr 1 nix-instantiate --eval --strict --show-trace "lang/$i.nix" \ + expectStderr 1 nix-instantiate $flags "lang/$i.nix" \ | sed "s!$(pwd)!/pwd!g" > "lang/$i.err" then diffAndAccept "$i" err err.exp diff --git a/tests/functional/lang/eval-fail-addErrorContext-example.err.exp b/tests/functional/lang/eval-fail-addErrorContext-example.err.exp new file mode 100644 index 000000000..4fad8f5c8 --- /dev/null +++ b/tests/functional/lang/eval-fail-addErrorContext-example.err.exp @@ -0,0 +1,24 @@ +error: + … while counting down; n = 10 + + … while counting down; n = 9 + + … while counting down; n = 8 + + … while counting down; n = 7 + + … while counting down; n = 6 + + … while counting down; n = 5 + + … while counting down; n = 4 + + … while counting down; n = 3 + + … while counting down; n = 2 + + … while counting down; n = 1 + + (stack trace truncated; use '--show-trace' to show the full, detailed trace) + + error: kaboom diff --git a/tests/functional/lang/eval-fail-addErrorContext-example.flags b/tests/functional/lang/eval-fail-addErrorContext-example.flags new file mode 100644 index 000000000..9b1f6458f --- /dev/null +++ b/tests/functional/lang/eval-fail-addErrorContext-example.flags @@ -0,0 +1 @@ +--eval --strict --no-show-trace diff --git a/tests/functional/lang/eval-fail-addErrorContext-example.nix b/tests/functional/lang/eval-fail-addErrorContext-example.nix new file mode 100644 index 000000000..996b24688 --- /dev/null +++ b/tests/functional/lang/eval-fail-addErrorContext-example.nix @@ -0,0 +1,9 @@ +let + countDown = n: + if n == 0 + then throw "kaboom" + else + builtins.addErrorContext + "while counting down; n = ${toString n}" + ("x" + countDown (n - 1)); +in countDown 10 diff --git a/tests/functional/lang/eval-okay-baseNameOf.exp b/tests/functional/lang/eval-okay-baseNameOf.exp new file mode 100644 index 000000000..52c33a57c --- /dev/null +++ b/tests/functional/lang/eval-okay-baseNameOf.exp @@ -0,0 +1 @@ +"ok" diff --git a/tests/functional/lang/eval-okay-baseNameOf.nix b/tests/functional/lang/eval-okay-baseNameOf.nix new file mode 100644 index 000000000..a7afdd896 --- /dev/null +++ b/tests/functional/lang/eval-okay-baseNameOf.nix @@ -0,0 +1,32 @@ +assert baseNameOf "" == ""; +assert baseNameOf "." == "."; +assert baseNameOf ".." == ".."; +assert baseNameOf "a" == "a"; +assert baseNameOf "a." == "a."; +assert baseNameOf "a.." == "a.."; +assert baseNameOf "a.b" == "a.b"; +assert baseNameOf "a.b." == "a.b."; +assert baseNameOf "a.b.." == "a.b.."; +assert baseNameOf "a/" == "a"; +assert baseNameOf "a/." == "."; +assert baseNameOf "a/.." == ".."; +assert baseNameOf "a/b" == "b"; +assert baseNameOf "a/b." == "b."; +assert baseNameOf "a/b.." == "b.."; +assert baseNameOf "a/b/c" == "c"; +assert baseNameOf "a/b/c." == "c."; +assert baseNameOf "a/b/c.." == "c.."; +assert baseNameOf "a/b/c/d" == "d"; +assert baseNameOf "a/b/c/d." == "d."; +assert baseNameOf "a\\b" == "a\\b"; +assert baseNameOf "C:a" == "C:a"; +assert baseNameOf "a//b" == "b"; + +# It's been like this for close to a decade. We ought to commit to it. +# https://github.com/NixOS/nix/pull/582#issuecomment-121014450 +assert baseNameOf "a//" == ""; + +assert baseNameOf ./foo == "foo"; +assert baseNameOf ./foo/bar == "bar"; + +"ok" diff --git a/tests/functional/local-store.sh b/tests/functional/local-store.sh deleted file mode 100644 index f7c8eb3f1..000000000 --- a/tests/functional/local-store.sh +++ /dev/null @@ -1,22 +0,0 @@ -source common.sh - -cd $TEST_ROOT - -echo example > example.txt -mkdir -p ./x - -NIX_STORE_DIR=$TEST_ROOT/x - -CORRECT_PATH=$(nix-store --store ./x --add example.txt) - -PATH1=$(nix path-info --store ./x $CORRECT_PATH) -[ $CORRECT_PATH == $PATH1 ] - -PATH2=$(nix path-info --store "$PWD/x" $CORRECT_PATH) -[ $CORRECT_PATH == $PATH2 ] - -PATH3=$(nix path-info --store "local?root=$PWD/x" $CORRECT_PATH) -[ $CORRECT_PATH == $PATH3 ] - -# Ensure store info trusted works with local store -nix --store ./x store info --json | jq -e '.trusted' diff --git a/tests/functional/local.mk b/tests/functional/local.mk index 8bb8e3600..ca9837d32 100644 --- a/tests/functional/local.mk +++ b/tests/functional/local.mk @@ -83,7 +83,7 @@ nix_tests = \ export.sh \ config.sh \ add.sh \ - local-store.sh \ + chroot-store.sh \ filter-source.sh \ misc.sh \ dump-db.sh \ diff --git a/tests/functional/plugins/local.mk b/tests/functional/plugins/local.mk index 40350aa96..2314e1341 100644 --- a/tests/functional/plugins/local.mk +++ b/tests/functional/plugins/local.mk @@ -8,4 +8,4 @@ libplugintest_ALLOW_UNDEFINED := 1 libplugintest_EXCLUDE_FROM_LIBRARY_LIST := 1 -libplugintest_CXXFLAGS := -I src/libutil -I src/libstore -I src/libexpr -I src/libfetchers +libplugintest_CXXFLAGS := $(INCLUDE_libutil) $(INCLUDE_libstore) $(INCLUDE_libexpr) $(INCLUDE_libfetchers) diff --git a/tests/functional/repl.sh b/tests/functional/repl.sh index 82267cbf7..95d8a7766 100644 --- a/tests/functional/repl.sh +++ b/tests/functional/repl.sh @@ -49,6 +49,9 @@ testRepl () { | grep "attribute 'currentSystem' missing" nix repl "${nixArgs[@]}" 2>&1 <<< "builtins.currentSystem" \ | grep "$(nix-instantiate --eval -E 'builtins.currentSystem')" + + expectStderr 1 nix repl ${testDir}/simple.nix \ + | grepQuiet -s "error: path '$testDir/simple.nix' is not a flake" } # Simple test, try building a drv diff --git a/tests/functional/test-libstoreconsumer/local.mk b/tests/functional/test-libstoreconsumer/local.mk index a1825c405..3e8581c57 100644 --- a/tests/functional/test-libstoreconsumer/local.mk +++ b/tests/functional/test-libstoreconsumer/local.mk @@ -8,7 +8,7 @@ test-libstoreconsumer_INSTALL_DIR := test-libstoreconsumer_SOURCES := \ $(wildcard $(d)/*.cc) \ -test-libstoreconsumer_CXXFLAGS += -I src/libutil -I src/libstore +test-libstoreconsumer_CXXFLAGS += $(INCLUDE_libutil) $(INCLUDE_libstore) test-libstoreconsumer_LIBS = libstore libutil diff --git a/tests/functional/user-envs.sh b/tests/functional/user-envs.sh index dcd6b1b97..7c643f355 100644 --- a/tests/functional/user-envs.sh +++ b/tests/functional/user-envs.sh @@ -189,3 +189,9 @@ nix-env --set $outPath10 [ "$(nix-store -q --resolve $profiles/test)" = $outPath10 ] nix-env --set $drvPath10 [ "$(nix-store -q --resolve $profiles/test)" = $outPath10 ] + +# Test the case where $HOME contains a symlink. +mkdir -p $TEST_ROOT/real-home/alice/.nix-defexpr/channels +ln -sfn $TEST_ROOT/real-home $TEST_ROOT/home +ln -sfn $(pwd)/user-envs.nix $TEST_ROOT/home/alice/.nix-defexpr/channels/foo +HOME=$TEST_ROOT/home/alice nix-env -i foo-0.1 diff --git a/tests/nixos/default.nix b/tests/nixos/default.nix index 98de31e13..627728424 100644 --- a/tests/nixos/default.nix +++ b/tests/nixos/default.nix @@ -158,4 +158,6 @@ in fetch-git = runNixOSTestFor "x86_64-linux" ./fetch-git; ca-fd-leak = runNixOSTestFor "x86_64-linux" ./ca-fd-leak; + + gzip-content-encoding = runNixOSTestFor "x86_64-linux" ./gzip-content-encoding.nix; } diff --git a/tests/nixos/github-flakes.nix b/tests/nixos/github-flakes.nix index 6f8a5b9d8..221045009 100644 --- a/tests/nixos/github-flakes.nix +++ b/tests/nixos/github-flakes.nix @@ -187,9 +187,14 @@ in client.succeed("nix flake metadata nixpkgs --tarball-ttl 0 >&2") # Test fetchTree on a github URL. - hash = client.succeed(f"nix eval --raw --expr '(fetchTree {info['url']}).narHash'") + hash = client.succeed(f"nix eval --no-trust-tarballs-from-git-forges --raw --expr '(fetchTree {info['url']}).narHash'") assert hash == info['locked']['narHash'] + # Fetching without a narHash should succeed if trust-github is set and fail otherwise. + client.succeed(f"nix eval --raw --expr 'builtins.fetchTree github:github:fancy-enterprise/private-flake/{info['revision']}'") + out = client.fail(f"nix eval --no-trust-tarballs-from-git-forges --raw --expr 'builtins.fetchTree github:github:fancy-enterprise/private-flake/{info['revision']}' 2>&1") + assert "will not fetch unlocked input" in out, "--no-trust-tarballs-from-git-forges did not fail with the expected error" + # Shut down the web server. The flake should be cached on the client. github.succeed("systemctl stop httpd.service") diff --git a/tests/nixos/gzip-content-encoding.nix b/tests/nixos/gzip-content-encoding.nix new file mode 100644 index 000000000..a5a0033fd --- /dev/null +++ b/tests/nixos/gzip-content-encoding.nix @@ -0,0 +1,71 @@ +# Test that compressed files fetched from server with compressed responses +# do not get excessively decompressed. +# E.g. fetching a zstd compressed tarball from a server, +# which compresses the response with `Content-Encoding: gzip`. +# The expected result is that the fetched file is a zstd archive. + +{ lib, config, ... }: + +let + pkgs = config.nodes.machine.nixpkgs.pkgs; + + ztdCompressedFile = pkgs.stdenv.mkDerivation { + name = "dummy-zstd-compressed-archive"; + dontUnpack = true; + nativeBuildInputs = with pkgs; [ zstd ]; + buildPhase = '' + mkdir archive + for _ in {1..100}; do echo "lorem" > archive/file1; done + for _ in {1..100}; do echo "ipsum" > archive/file2; done + tar --zstd -cf archive.tar.zst archive + ''; + installPhase = '' + install -Dm 644 -T archive.tar.zst $out/share/archive + ''; + }; + + fileCmd = "${pkgs.file}/bin/file"; +in + +{ + name = "gzip-content-encoding"; + + nodes = + { machine = + { config, pkgs, ... }: + { networking.firewall.allowedTCPPorts = [ 80 ]; + + services.nginx.enable = true; + services.nginx.virtualHosts."localhost" = + { root = "${ztdCompressedFile}/share/"; + # Make sure that nginx really tries to compress the + # file on the fly with no regard to size/mime. + # http://nginx.org/en/docs/http/ngx_http_gzip_module.html + extraConfig = '' + gzip on; + gzip_types *; + gzip_proxied any; + gzip_min_length 0; + ''; + }; + virtualisation.writableStore = true; + virtualisation.additionalPaths = with pkgs; [ file ]; + nix.settings.substituters = lib.mkForce [ ]; + }; + }; + + # Check that when nix-prefetch-url is used with a zst tarball it does not get decompressed. + testScript = { nodes }: '' + # fmt: off + start_all() + + machine.wait_for_unit("nginx.service") + machine.succeed(""" + # Make sure that the file is properly compressed as the test would be meaningless otherwise + curl --compressed -v http://localhost/archive |& tr -s ' ' |& grep --ignore-case 'content-encoding: gzip' + archive_path=$(nix-prefetch-url http://localhost/archive --print-path | tail -n1) + [[ $(${fileCmd} --brief --mime-type $archive_path) == "application/zstd" ]] + tar --zstd -xf $archive_path + """) + ''; +} diff --git a/tests/unit/libexpr-support/tests/nix_api_expr.hh b/tests/unit/libexpr-support/tests/nix_api_expr.hh new file mode 100644 index 000000000..d1840d034 --- /dev/null +++ b/tests/unit/libexpr-support/tests/nix_api_expr.hh @@ -0,0 +1,31 @@ +#pragma once +///@file +#include "nix_api_expr.h" +#include "nix_api_value.h" +#include "tests/nix_api_store.hh" + +#include + +namespace nixC { + +class nix_api_expr_test : public nix_api_store_test +{ +protected: + + nix_api_expr_test() + { + nix_libexpr_init(ctx); + state = nix_state_create(nullptr, nullptr, store); + value = nix_alloc_value(nullptr, state); + } + ~nix_api_expr_test() + { + nix_gc_decref(nullptr, value); + nix_state_free(state); + } + + EvalState * state; + Value * value; +}; + +} diff --git a/tests/unit/libexpr/local.mk b/tests/unit/libexpr/local.mk index 25810ad9c..8df1a5207 100644 --- a/tests/unit/libexpr/local.mk +++ b/tests/unit/libexpr/local.mk @@ -23,15 +23,18 @@ libexpr-tests_EXTRA_INCLUDES = \ -I tests/unit/libexpr-support \ -I tests/unit/libstore-support \ -I tests/unit/libutil-support \ - -I src/libexpr \ - -I src/libfetchers \ - -I src/libstore \ - -I src/libutil + $(INCLUDE_libexpr) \ + $(INCLUDE_libexprc) \ + $(INCLUDE_libfetchers) \ + $(INCLUDE_libstore) \ + $(INCLUDE_libstorec) \ + $(INCLUDE_libutil) \ + $(INCLUDE_libutilc) libexpr-tests_CXXFLAGS += $(libexpr-tests_EXTRA_INCLUDES) libexpr-tests_LIBS = \ libexpr-test-support libstore-test-support libutils-test-support \ - libexpr libfetchers libstore libutil + libexpr libexprc libfetchers libstore libstorec libutil libutilc libexpr-tests_LDFLAGS := -lrapidcheck $(GTEST_LIBS) -lgmock diff --git a/tests/unit/libexpr/nix_api_expr.cc b/tests/unit/libexpr/nix_api_expr.cc new file mode 100644 index 000000000..9d54a62f8 --- /dev/null +++ b/tests/unit/libexpr/nix_api_expr.cc @@ -0,0 +1,100 @@ +#include "nix_api_store.h" +#include "nix_api_store_internal.h" +#include "nix_api_util.h" +#include "nix_api_util_internal.h" +#include "nix_api_expr.h" +#include "nix_api_value.h" + +#include "tests/nix_api_expr.hh" + +#include + +namespace nixC { + +TEST_F(nix_api_expr_test, nix_expr_eval_from_string) +{ + nix_expr_eval_from_string(nullptr, state, "builtins.nixVersion", ".", value); + nix_value_force(nullptr, state, value); + auto result = nix_get_string(nullptr, value); + + ASSERT_STREQ(PACKAGE_VERSION, result); +} + +TEST_F(nix_api_expr_test, nix_expr_eval_add_numbers) +{ + nix_expr_eval_from_string(nullptr, state, "1 + 1", ".", value); + nix_value_force(nullptr, state, value); + auto result = nix_get_int(nullptr, value); + + ASSERT_EQ(2, result); +} + +TEST_F(nix_api_expr_test, nix_expr_eval_drv) +{ + auto expr = R"(derivation { name = "myname"; builder = "mybuilder"; system = "mysystem"; })"; + nix_expr_eval_from_string(nullptr, state, expr, ".", value); + ASSERT_EQ(NIX_TYPE_ATTRS, nix_get_type(nullptr, value)); + + EvalState * stateFn = nix_state_create(nullptr, nullptr, store); + Value * valueFn = nix_alloc_value(nullptr, state); + nix_expr_eval_from_string(nullptr, stateFn, "builtins.toString", ".", valueFn); + ASSERT_EQ(NIX_TYPE_FUNCTION, nix_get_type(nullptr, valueFn)); + + EvalState * stateResult = nix_state_create(nullptr, nullptr, store); + Value * valueResult = nix_alloc_value(nullptr, stateResult); + nix_value_call(ctx, stateResult, valueFn, value, valueResult); + ASSERT_EQ(NIX_TYPE_STRING, nix_get_type(nullptr, valueResult)); + + std::string p = nix_get_string(nullptr, valueResult); + std::string pEnd = "-myname"; + ASSERT_EQ(pEnd, p.substr(p.size() - pEnd.size())); + + // Clean up + nix_gc_decref(nullptr, valueFn); + nix_state_free(stateFn); + + nix_gc_decref(nullptr, valueResult); + nix_state_free(stateResult); +} + +TEST_F(nix_api_expr_test, nix_build_drv) +{ + auto expr = R"(derivation { name = "myname"; + system = builtins.currentSystem; + builder = "/bin/sh"; + args = [ "-c" "echo foo > $out" ]; + })"; + nix_expr_eval_from_string(nullptr, state, expr, ".", value); + + Value * drvPathValue = nix_get_attr_byname(nullptr, value, state, "drvPath"); + const char * drvPath = nix_get_string(nullptr, drvPathValue); + + std::string p = drvPath; + std::string pEnd = "-myname.drv"; + ASSERT_EQ(pEnd, p.substr(p.size() - pEnd.size())); + + StorePath * drvStorePath = nix_store_parse_path(ctx, store, drvPath); + ASSERT_EQ(true, nix_store_is_valid_path(ctx, store, drvStorePath)); + + Value * outPathValue = nix_get_attr_byname(ctx, value, state, "outPath"); + const char * outPath = nix_get_string(ctx, outPathValue); + + p = outPath; + pEnd = "-myname"; + ASSERT_EQ(pEnd, p.substr(p.size() - pEnd.size())); + ASSERT_EQ(true, drvStorePath->path.isDerivation()); + + StorePath * outStorePath = nix_store_parse_path(ctx, store, outPath); + ASSERT_EQ(false, nix_store_is_valid_path(ctx, store, outStorePath)); + + // TODO figure out why fails. + // `make libexpr-tests_RUN` works, but `nix build .` enters an infinite loop + /* nix_store_realise(ctx, store, drvStorePath, nullptr, nullptr); */ + /* auto is_valid_path = nix_store_is_valid_path(ctx, store, outStorePath); */ + /* ASSERT_EQ(true, is_valid_path); */ + + // Clean up + nix_store_path_free(drvStorePath); + nix_store_path_free(outStorePath); +} +} diff --git a/tests/unit/libexpr/nix_api_external.cc b/tests/unit/libexpr/nix_api_external.cc new file mode 100644 index 000000000..7e2caed1b --- /dev/null +++ b/tests/unit/libexpr/nix_api_external.cc @@ -0,0 +1,63 @@ +#include "nix_api_store.h" +#include "nix_api_store_internal.h" +#include "nix_api_util.h" +#include "nix_api_util_internal.h" +#include "nix_api_expr.h" +#include "nix_api_expr_internal.h" +#include "nix_api_value.h" +#include "nix_api_external.h" +#include "tests/nix_api_expr.hh" + +#include + +namespace nixC { + +class MyExternalValueDesc : public NixCExternalValueDesc +{ +public: + MyExternalValueDesc(int x) + : _x(x) + { + print = print_function; + showType = show_type_function; + typeOf = type_of_function; + } + +private: + int _x; + static void print_function(void * self, nix_printer * printer) {} + + static void show_type_function(void * self, nix_string_return * res) {} + + static void type_of_function(void * self, nix_string_return * res) + { + MyExternalValueDesc * obj = static_cast(self); + + std::string type_string = "nix-external_x); + type_string += " )>"; + res->str = &*type_string.begin(); + } +}; + +TEST_F(nix_api_expr_test, nix_expr_eval_external) +{ + MyExternalValueDesc * external = new MyExternalValueDesc(42); + ExternalValue * val = nix_create_external_value(ctx, external, external); + nix_init_external(ctx, value, val); + + EvalState * stateResult = nix_state_create(nullptr, nullptr, store); + Value * valueResult = nix_alloc_value(nullptr, stateResult); + + EvalState * stateFn = nix_state_create(nullptr, nullptr, store); + Value * valueFn = nix_alloc_value(nullptr, stateFn); + + nix_expr_eval_from_string(nullptr, state, "builtins.typeOf", ".", valueFn); + + ASSERT_EQ(NIX_TYPE_EXTERNAL, nix_get_type(nullptr, value)); + + nix_value_call(ctx, state, valueFn, value, valueResult); + + ASSERT_STREQ("nix-external", nix_get_string(nullptr, valueResult)); +} +} diff --git a/tests/unit/libexpr/nix_api_value.cc b/tests/unit/libexpr/nix_api_value.cc new file mode 100644 index 000000000..726960638 --- /dev/null +++ b/tests/unit/libexpr/nix_api_value.cc @@ -0,0 +1,184 @@ +#include "nix_api_store.h" +#include "nix_api_store_internal.h" +#include "nix_api_util.h" +#include "nix_api_util_internal.h" +#include "nix_api_expr.h" +#include "nix_api_value.h" + +#include "tests/nix_api_expr.hh" + +#include +#include + +namespace nixC { + +TEST_F(nix_api_expr_test, nix_value_set_get_int) +{ + ASSERT_EQ(0, nix_get_int(ctx, nullptr)); + ASSERT_DEATH(nix_get_int(ctx, value), ""); + + int myInt = 1; + nix_init_int(ctx, value, myInt); + + ASSERT_EQ(myInt, nix_get_int(ctx, value)); + ASSERT_STREQ("an integer", nix_get_typename(ctx, value)); + ASSERT_EQ(NIX_TYPE_INT, nix_get_type(ctx, value)); +} + +TEST_F(nix_api_expr_test, nix_value_set_get_float) +{ + ASSERT_FLOAT_EQ(0.0, nix_get_float(ctx, nullptr)); + ASSERT_DEATH(nix_get_float(ctx, value), ""); + + float myDouble = 1.0; + nix_init_float(ctx, value, myDouble); + + ASSERT_FLOAT_EQ(myDouble, nix_get_float(ctx, value)); + ASSERT_STREQ("a float", nix_get_typename(ctx, value)); + ASSERT_EQ(NIX_TYPE_FLOAT, nix_get_type(ctx, value)); +} + +TEST_F(nix_api_expr_test, nix_value_set_get_bool) +{ + ASSERT_EQ(false, nix_get_bool(ctx, nullptr)); + ASSERT_DEATH(nix_get_bool(ctx, value), ""); + + bool myBool = true; + nix_init_bool(ctx, value, myBool); + + ASSERT_EQ(myBool, nix_get_bool(ctx, value)); + ASSERT_STREQ("a Boolean", nix_get_typename(ctx, value)); + ASSERT_EQ(NIX_TYPE_BOOL, nix_get_type(ctx, value)); +} + +TEST_F(nix_api_expr_test, nix_value_set_get_string) +{ + ASSERT_EQ(nullptr, nix_get_string(ctx, nullptr)); + ASSERT_DEATH(nix_get_string(ctx, value), ""); + + const char * myString = "some string"; + nix_init_string(ctx, value, myString); + + ASSERT_STREQ(myString, nix_get_string(ctx, value)); + ASSERT_STREQ("a string", nix_get_typename(ctx, value)); + ASSERT_EQ(NIX_TYPE_STRING, nix_get_type(ctx, value)); +} + +TEST_F(nix_api_expr_test, nix_value_set_get_null) +{ + ASSERT_DEATH(nix_get_typename(ctx, value), ""); + + nix_init_null(ctx, value); + + ASSERT_STREQ("null", nix_get_typename(ctx, value)); + ASSERT_EQ(NIX_TYPE_NULL, nix_get_type(ctx, value)); +} + +TEST_F(nix_api_expr_test, nix_value_set_get_path) +{ + ASSERT_EQ(nullptr, nix_get_path_string(ctx, nullptr)); + ASSERT_DEATH(nix_get_path_string(ctx, value), ""); + + const char * p = "/nix/store/40s0qmrfb45vlh6610rk29ym318dswdr-myname"; + nix_init_path_string(ctx, state, value, p); + + ASSERT_STREQ(p, nix_get_path_string(ctx, value)); + ASSERT_STREQ("a path", nix_get_typename(ctx, value)); + ASSERT_EQ(NIX_TYPE_PATH, nix_get_type(ctx, value)); +} + +TEST_F(nix_api_expr_test, nix_build_and_init_list) +{ + ASSERT_EQ(nullptr, nix_get_list_byidx(ctx, nullptr, state, 0)); + ASSERT_EQ(0, nix_get_list_size(ctx, nullptr)); + + ASSERT_DEATH(nix_get_list_byidx(ctx, value, state, 0), ""); + ASSERT_DEATH(nix_get_list_size(ctx, value), ""); + + int size = 10; + ListBuilder * builder = nix_make_list_builder(ctx, state, size); + + Value * intValue = nix_alloc_value(ctx, state); + nix_init_int(ctx, intValue, 42); + nix_list_builder_insert(ctx, builder, 0, intValue); + nix_make_list(ctx, builder, value); + nix_list_builder_free(builder); + + ASSERT_EQ(42, nix_get_int(ctx, nix_get_list_byidx(ctx, value, state, 0))); + ASSERT_EQ(nullptr, nix_get_list_byidx(ctx, value, state, 1)); + ASSERT_EQ(10, nix_get_list_size(ctx, value)); + + ASSERT_STREQ("a list", nix_get_typename(ctx, value)); + ASSERT_EQ(NIX_TYPE_LIST, nix_get_type(ctx, value)); + + // Clean up + nix_gc_decref(ctx, intValue); +} + +TEST_F(nix_api_expr_test, nix_build_and_init_attr) +{ + ASSERT_EQ(nullptr, nix_get_attr_byname(ctx, nullptr, state, 0)); + ASSERT_EQ(nullptr, nix_get_attr_byidx(ctx, nullptr, state, 0, nullptr)); + ASSERT_EQ(nullptr, nix_get_attr_name_byidx(ctx, nullptr, state, 0)); + ASSERT_EQ(0, nix_get_attrs_size(ctx, nullptr)); + ASSERT_EQ(false, nix_has_attr_byname(ctx, nullptr, state, "no-value")); + + ASSERT_DEATH(nix_get_attr_byname(ctx, value, state, 0), ""); + ASSERT_DEATH(nix_get_attr_byidx(ctx, value, state, 0, nullptr), ""); + ASSERT_DEATH(nix_get_attr_name_byidx(ctx, value, state, 0), ""); + ASSERT_DEATH(nix_get_attrs_size(ctx, value), ""); + ASSERT_DEATH(nix_has_attr_byname(ctx, value, state, "no-value"), ""); + + int size = 10; + const char ** out_name = (const char **) malloc(sizeof(char *)); + + BindingsBuilder * builder = nix_make_bindings_builder(ctx, state, size); + + Value * intValue = nix_alloc_value(ctx, state); + nix_init_int(ctx, intValue, 42); + + Value * stringValue = nix_alloc_value(ctx, state); + nix_init_string(ctx, stringValue, "foo"); + + nix_bindings_builder_insert(ctx, builder, "a", intValue); + nix_bindings_builder_insert(ctx, builder, "b", stringValue); + nix_make_attrs(ctx, value, builder); + nix_bindings_builder_free(builder); + + ASSERT_EQ(2, nix_get_attrs_size(ctx, value)); + + Value * out_value = nix_get_attr_byname(ctx, value, state, "a"); + ASSERT_EQ(42, nix_get_int(ctx, out_value)); + nix_gc_decref(ctx, out_value); + + out_value = nix_get_attr_byidx(ctx, value, state, 0, out_name); + ASSERT_EQ(42, nix_get_int(ctx, out_value)); + ASSERT_STREQ("a", *out_name); + nix_gc_decref(ctx, out_value); + + ASSERT_STREQ("a", nix_get_attr_name_byidx(ctx, value, state, 0)); + + ASSERT_EQ(true, nix_has_attr_byname(ctx, value, state, "b")); + ASSERT_EQ(false, nix_has_attr_byname(ctx, value, state, "no-value")); + + out_value = nix_get_attr_byname(ctx, value, state, "b"); + ASSERT_STREQ("foo", nix_get_string(ctx, out_value)); + nix_gc_decref(nullptr, out_value); + + out_value = nix_get_attr_byidx(ctx, value, state, 1, out_name); + ASSERT_STREQ("foo", nix_get_string(ctx, out_value)); + ASSERT_STREQ("b", *out_name); + nix_gc_decref(nullptr, out_value); + + ASSERT_STREQ("b", nix_get_attr_name_byidx(ctx, value, state, 1)); + + ASSERT_STREQ("a set", nix_get_typename(ctx, value)); + ASSERT_EQ(NIX_TYPE_ATTRS, nix_get_type(ctx, value)); + + // Clean up + nix_gc_decref(ctx, intValue); + nix_gc_decref(ctx, stringValue); + free(out_name); +} + +} diff --git a/tests/unit/libexpr/primops.cc b/tests/unit/libexpr/primops.cc index b1426edae..92319e0c3 100644 --- a/tests/unit/libexpr/primops.cc +++ b/tests/unit/libexpr/primops.cc @@ -91,7 +91,7 @@ namespace nix { } TEST_F(PrimOpTest, getEnv) { - setenv("_NIX_UNIT_TEST_ENV_VALUE", "test value", 1); + setEnv("_NIX_UNIT_TEST_ENV_VALUE", "test value"); auto v = eval("builtins.getEnv \"_NIX_UNIT_TEST_ENV_VALUE\""); ASSERT_THAT(v, IsStringEq("test value")); } diff --git a/tests/unit/libfetchers/local.mk b/tests/unit/libfetchers/local.mk new file mode 100644 index 000000000..e9f659fd7 --- /dev/null +++ b/tests/unit/libfetchers/local.mk @@ -0,0 +1,32 @@ +check: libfetchers-tests_RUN + +programs += libfetchers-tests + +libfetchers-tests_NAME = libnixfetchers-tests + +libfetchers-tests_ENV := _NIX_TEST_UNIT_DATA=$(d)/data + +libfetchers-tests_DIR := $(d) + +ifeq ($(INSTALL_UNIT_TESTS), yes) + libfetchers-tests_INSTALL_DIR := $(checkbindir) +else + libfetchers-tests_INSTALL_DIR := +endif + +libfetchers-tests_SOURCES := $(wildcard $(d)/*.cc) + +libfetchers-tests_EXTRA_INCLUDES = \ + -I tests/unit/libstore-support \ + -I tests/unit/libutil-support \ + $(INCLUDE_libfetchers) \ + $(INCLUDE_libstore) \ + $(INCLUDE_libutil) + +libfetchers-tests_CXXFLAGS += $(libfetchers-tests_EXTRA_INCLUDES) + +libfetchers-tests_LIBS = \ + libstore-test-support libutil-test-support \ + libfetchers libstore libutil + +libfetchers-tests_LDFLAGS := -lrapidcheck $(GTEST_LIBS) diff --git a/tests/unit/libfetchers/public-key.cc b/tests/unit/libfetchers/public-key.cc new file mode 100644 index 000000000..fcd5c3af0 --- /dev/null +++ b/tests/unit/libfetchers/public-key.cc @@ -0,0 +1,18 @@ +#include +#include "fetchers.hh" +#include "json-utils.hh" + +namespace nix { + TEST(PublicKey, jsonSerialization) { + auto json = nlohmann::json(fetchers::PublicKey { .key = "ABCDE" }); + + ASSERT_EQ(json, R"({ "key": "ABCDE", "type": "ssh-ed25519" })"_json); + } + TEST(PublicKey, jsonDeserialization) { + auto pubKeyJson = R"({ "key": "ABCDE", "type": "ssh-ed25519" })"_json; + fetchers::PublicKey pubKey = pubKeyJson; + + ASSERT_EQ(pubKey.key, "ABCDE"); + ASSERT_EQ(pubKey.type, "ssh-ed25519"); + } +} diff --git a/tests/unit/libstore-support/tests/nix_api_store.hh b/tests/unit/libstore-support/tests/nix_api_store.hh new file mode 100644 index 000000000..a2d35d083 --- /dev/null +++ b/tests/unit/libstore-support/tests/nix_api_store.hh @@ -0,0 +1,66 @@ +#pragma once +///@file +#include "tests/nix_api_util.hh" + +#include "file-system.hh" + +#include "nix_api_store.h" +#include "nix_api_store_internal.h" + +#include +#include + +namespace fs = std::filesystem; + +namespace nixC { +class nix_api_store_test : public nix_api_util_context +{ +public: + nix_api_store_test() + { + nix_libstore_init(ctx); + init_local_store(); + }; + + ~nix_api_store_test() override + { + nix_store_free(store); + + for (auto & path : fs::recursive_directory_iterator(nixDir)) { + fs::permissions(path, fs::perms::owner_all); + } + fs::remove_all(nixDir); + } + + Store * store; + std::string nixDir; + std::string nixStoreDir; + +protected: + void init_local_store() + { +#ifdef _WIN32 + // no `mkdtemp` with MinGW + auto tmpl = nix::defaultTempDir() + "/tests_nix-store."; + for (size_t i = 0; true; ++i) { + nixDir = tmpl + std::string { i }; + if (fs::create_directory(nixDir)) break; + } +#else + auto tmpl = nix::defaultTempDir() + "/tests_nix-store.XXXXXX"; + nixDir = mkdtemp((char *) tmpl.c_str()); +#endif + + nixStoreDir = nixDir + "/my_nix_store"; + + // Options documented in `nix help-stores` + const char * p1[] = {"store", nixStoreDir.c_str()}; + const char * p2[] = {"state", (new std::string(nixDir + "/my_state"))->c_str()}; + const char * p3[] = {"log", (new std::string(nixDir + "/my_log"))->c_str()}; + + const char ** params[] = {p1, p2, p3, nullptr}; + + store = nix_store_open(ctx, "local", params); + } +}; +} diff --git a/tests/unit/libstore/local.mk b/tests/unit/libstore/local.mk index 63f6d011f..b8f895fad 100644 --- a/tests/unit/libstore/local.mk +++ b/tests/unit/libstore/local.mk @@ -19,13 +19,15 @@ libstore-tests_SOURCES := $(wildcard $(d)/*.cc) libstore-tests_EXTRA_INCLUDES = \ -I tests/unit/libstore-support \ -I tests/unit/libutil-support \ - -I src/libstore \ - -I src/libutil + $(INCLUDE_libstore) \ + $(INCLUDE_libstorec) \ + $(INCLUDE_libutil) \ + $(INCLUDE_libutilc) libstore-tests_CXXFLAGS += $(libstore-tests_EXTRA_INCLUDES) libstore-tests_LIBS = \ libstore-test-support libutil-test-support \ - libstore libutil + libstore libstorec libutil libutilc libstore-tests_LDFLAGS := -lrapidcheck $(GTEST_LIBS) diff --git a/tests/unit/libstore/nix_api_store.cc b/tests/unit/libstore/nix_api_store.cc new file mode 100644 index 000000000..a31d66a4c --- /dev/null +++ b/tests/unit/libstore/nix_api_store.cc @@ -0,0 +1,93 @@ +#include "nix_api_util.h" +#include "nix_api_util_internal.h" +#include "nix_api_store.h" +#include "nix_api_store_internal.h" + +#include "tests/nix_api_store.hh" + +namespace nixC { + +void observe_string_cb(const char * start, unsigned int n, std::string * user_data) +{ + *user_data = std::string(start); +} + +std::string PATH_SUFFIX = "/g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-name"; + +TEST_F(nix_api_util_context, nix_libstore_init) +{ + auto ret = nix_libstore_init(ctx); + ASSERT_EQ(NIX_OK, ret); +} + +TEST_F(nix_api_store_test, nix_store_get_uri) +{ + std::string str; + auto ret = nix_store_get_uri(ctx, store, (void *) observe_string_cb, &str); + ASSERT_EQ(NIX_OK, ret); + ASSERT_STREQ("local", str.c_str()); +} + +TEST_F(nix_api_store_test, InvalidPathFails) +{ + nix_store_parse_path(ctx, store, "invalid-path"); + ASSERT_EQ(ctx->last_err_code, NIX_ERR_NIX_ERROR); +} + +TEST_F(nix_api_store_test, ReturnsValidStorePath) +{ + StorePath * result = nix_store_parse_path(ctx, store, (nixStoreDir + PATH_SUFFIX).c_str()); + ASSERT_NE(result, nullptr); + ASSERT_STREQ("name", result->path.name().data()); + ASSERT_STREQ(PATH_SUFFIX.substr(1).c_str(), result->path.to_string().data()); +} + +TEST_F(nix_api_store_test, SetsLastErrCodeToNixOk) +{ + nix_store_parse_path(ctx, store, (nixStoreDir + PATH_SUFFIX).c_str()); + ASSERT_EQ(ctx->last_err_code, NIX_OK); +} + +TEST_F(nix_api_store_test, DoesNotCrashWhenContextIsNull) +{ + ASSERT_NO_THROW(nix_store_parse_path(ctx, store, (nixStoreDir + PATH_SUFFIX).c_str())); +} + +TEST_F(nix_api_store_test, get_version) +{ + std::string str; + auto ret = nix_store_get_version(ctx, store, (void *) observe_string_cb, &str); + ASSERT_EQ(NIX_OK, ret); + ASSERT_STREQ(PACKAGE_VERSION, str.c_str()); +} + +TEST_F(nix_api_util_context, nix_store_open_dummy) +{ + nix_libstore_init(ctx); + Store * store = nix_store_open(ctx, "dummy://", nullptr); + ASSERT_EQ(NIX_OK, ctx->last_err_code); + ASSERT_STREQ("dummy", store->ptr->getUri().c_str()); + + std::string str; + nix_store_get_version(ctx, store, (void *) observe_string_cb, &str); + ASSERT_STREQ("", str.c_str()); + + nix_store_free(store); +} + +TEST_F(nix_api_util_context, nix_store_open_invalid) +{ + nix_libstore_init(ctx); + Store * store = nix_store_open(ctx, "invalid://", nullptr); + ASSERT_EQ(NIX_ERR_NIX_ERROR, ctx->last_err_code); + ASSERT_EQ(nullptr, store); + nix_store_free(store); +} + +TEST_F(nix_api_store_test, nix_store_is_valid_path_not_in_store) +{ + StorePath * path = nix_store_parse_path(ctx, store, (nixStoreDir + PATH_SUFFIX).c_str()); + ASSERT_EQ(false, nix_store_is_valid_path(ctx, store, path)); +} + +} diff --git a/tests/unit/libutil-support/tests/nix_api_util.hh b/tests/unit/libutil-support/tests/nix_api_util.hh new file mode 100644 index 000000000..0dfb38f7b --- /dev/null +++ b/tests/unit/libutil-support/tests/nix_api_util.hh @@ -0,0 +1,27 @@ +#pragma once +///@file +#include "nix_api_util.h" + +#include + +namespace nixC { + +class nix_api_util_context : public ::testing::Test +{ +protected: + + nix_api_util_context() + { + ctx = nix_c_context_create(); + nix_libutil_init(ctx); + }; + + ~nix_api_util_context() override + { + nix_c_context_free(ctx); + ctx = nullptr; + } + + nix_c_context * ctx; +}; +} diff --git a/tests/unit/libutil/json-utils.cc b/tests/unit/libutil/json-utils.cc index f0ce15c93..ffa667806 100644 --- a/tests/unit/libutil/json-utils.cc +++ b/tests/unit/libutil/json-utils.cc @@ -3,6 +3,7 @@ #include +#include "error.hh" #include "json-utils.hh" namespace nix { @@ -55,4 +56,108 @@ TEST(from_json, vectorOfOptionalInts) { ASSERT_FALSE(vals.at(1).has_value()); } +TEST(valueAt, simpleObject) { + auto simple = R"({ "hello": "world" })"_json; + + ASSERT_EQ(valueAt(getObject(simple), "hello"), "world"); + + auto nested = R"({ "hello": { "world": "" } })"_json; + + auto & nestedObject = valueAt(getObject(nested), "hello"); + + ASSERT_EQ(valueAt(nestedObject, "world"), ""); +} + +TEST(valueAt, missingKey) { + auto json = R"({ "hello": { "nested": "world" } })"_json; + + auto & obj = getObject(json); + + ASSERT_THROW(valueAt(obj, "foo"), Error); +} + +TEST(getObject, rightAssertions) { + auto simple = R"({ "object": {} })"_json; + + ASSERT_EQ(getObject(valueAt(getObject(simple), "object")), (nlohmann::json::object_t {})); + + auto nested = R"({ "object": { "object": {} } })"_json; + + auto & nestedObject = getObject(valueAt(getObject(nested), "object")); + + ASSERT_EQ(nestedObject, getObject(nlohmann::json::parse(R"({ "object": {} })"))); + ASSERT_EQ(getObject(valueAt(getObject(nestedObject), "object")), (nlohmann::json::object_t {})); +} + +TEST(getObject, wrongAssertions) { + auto json = R"({ "object": {}, "array": [], "string": "", "int": 0, "boolean": false })"_json; + + auto & obj = getObject(json); + + ASSERT_THROW(getObject(valueAt(obj, "array")), Error); + ASSERT_THROW(getObject(valueAt(obj, "string")), Error); + ASSERT_THROW(getObject(valueAt(obj, "int")), Error); + ASSERT_THROW(getObject(valueAt(obj, "boolean")), Error); +} + +TEST(getArray, rightAssertions) { + auto simple = R"({ "array": [] })"_json; + + ASSERT_EQ(getArray(valueAt(getObject(simple), "array")), (nlohmann::json::array_t {})); +} + +TEST(getArray, wrongAssertions) { + auto json = R"({ "object": {}, "array": [], "string": "", "int": 0, "boolean": false })"_json; + + ASSERT_THROW(getArray(valueAt(json, "object")), Error); + ASSERT_THROW(getArray(valueAt(json, "string")), Error); + ASSERT_THROW(getArray(valueAt(json, "int")), Error); + ASSERT_THROW(getArray(valueAt(json, "boolean")), Error); +} + +TEST(getString, rightAssertions) { + auto simple = R"({ "string": "" })"_json; + + ASSERT_EQ(getString(valueAt(getObject(simple), "string")), ""); +} + +TEST(getString, wrongAssertions) { + auto json = R"({ "object": {}, "array": [], "string": "", "int": 0, "boolean": false })"_json; + + ASSERT_THROW(getString(valueAt(json, "object")), Error); + ASSERT_THROW(getString(valueAt(json, "array")), Error); + ASSERT_THROW(getString(valueAt(json, "int")), Error); + ASSERT_THROW(getString(valueAt(json, "boolean")), Error); +} + +TEST(getInteger, rightAssertions) { + auto simple = R"({ "int": 0 })"_json; + + ASSERT_EQ(getInteger(valueAt(getObject(simple), "int")), 0); +} + +TEST(getInteger, wrongAssertions) { + auto json = R"({ "object": {}, "array": [], "string": "", "int": 0, "boolean": false })"_json; + + ASSERT_THROW(getInteger(valueAt(json, "object")), Error); + ASSERT_THROW(getInteger(valueAt(json, "array")), Error); + ASSERT_THROW(getInteger(valueAt(json, "string")), Error); + ASSERT_THROW(getInteger(valueAt(json, "boolean")), Error); +} + +TEST(getBoolean, rightAssertions) { + auto simple = R"({ "boolean": false })"_json; + + ASSERT_EQ(getBoolean(valueAt(getObject(simple), "boolean")), false); +} + +TEST(getBoolean, wrongAssertions) { + auto json = R"({ "object": {}, "array": [], "string": "", "int": 0, "boolean": false })"_json; + + ASSERT_THROW(getBoolean(valueAt(json, "object")), Error); + ASSERT_THROW(getBoolean(valueAt(json, "array")), Error); + ASSERT_THROW(getBoolean(valueAt(json, "string")), Error); + ASSERT_THROW(getBoolean(valueAt(json, "int")), Error); +} + } /* namespace nix */ diff --git a/tests/unit/libutil/local.mk b/tests/unit/libutil/local.mk index 930efb90b..39b4c0782 100644 --- a/tests/unit/libutil/local.mk +++ b/tests/unit/libutil/local.mk @@ -18,11 +18,12 @@ libutil-tests_SOURCES := $(wildcard $(d)/*.cc) libutil-tests_EXTRA_INCLUDES = \ -I tests/unit/libutil-support \ - -I src/libutil + $(INCLUDE_libutil) \ + $(INCLUDE_libutilc) libutil-tests_CXXFLAGS += $(libutil-tests_EXTRA_INCLUDES) -libutil-tests_LIBS = libutil-test-support libutil +libutil-tests_LIBS = libutil-test-support libutil libutilc libutil-tests_LDFLAGS := -lrapidcheck $(GTEST_LIBS) diff --git a/tests/unit/libutil/nix_api_util.cc b/tests/unit/libutil/nix_api_util.cc new file mode 100644 index 000000000..09f3f3e05 --- /dev/null +++ b/tests/unit/libutil/nix_api_util.cc @@ -0,0 +1,145 @@ +#include "config.hh" +#include "args.hh" +#include "nix_api_util.h" +#include "nix_api_util_internal.h" +#include "tests/nix_api_util.hh" + +#include + +namespace nixC { + +void observe_string_cb(const char * start, unsigned int n, std::string * user_data) +{ + *user_data = std::string(start); +} + +TEST_F(nix_api_util_context, nix_context_error) +{ + std::string err_msg_ref; + try { + throw nix::Error("testing error"); + } catch (nix::Error & e) { + err_msg_ref = e.what(); + nix_context_error(ctx); + } + ASSERT_EQ(ctx->last_err_code, NIX_ERR_NIX_ERROR); + ASSERT_EQ(ctx->name, "nix::Error"); + ASSERT_EQ(*ctx->last_err, err_msg_ref); + ASSERT_EQ(ctx->info->msg.str(), "testing error"); + + try { + throw std::runtime_error("testing exception"); + } catch (std::exception & e) { + err_msg_ref = e.what(); + nix_context_error(ctx); + } + ASSERT_EQ(ctx->last_err_code, NIX_ERR_UNKNOWN); + ASSERT_EQ(*ctx->last_err, err_msg_ref); +} + +TEST_F(nix_api_util_context, nix_set_err_msg) +{ + ASSERT_EQ(ctx->last_err_code, NIX_OK); + nix_set_err_msg(ctx, NIX_ERR_UNKNOWN, "unknown test error"); + ASSERT_EQ(ctx->last_err_code, NIX_ERR_UNKNOWN); + ASSERT_EQ(*ctx->last_err, "unknown test error"); +} + +TEST(nix_api_util, nix_version_get) +{ + ASSERT_EQ(std::string(nix_version_get()), PACKAGE_VERSION); +} + +struct MySettings : nix::Config +{ + nix::Setting settingSet{this, "empty", "setting-name", "Description"}; +}; + +MySettings mySettings; +static nix::GlobalConfig::Register rs(&mySettings); + +TEST_F(nix_api_util_context, nix_setting_get) +{ + ASSERT_EQ(ctx->last_err_code, NIX_OK); + std::string setting_value; + nix_err result = nix_setting_get(ctx, "invalid-key", (void *) observe_string_cb, &setting_value); + ASSERT_EQ(result, NIX_ERR_KEY); + + result = nix_setting_get(ctx, "setting-name", (void *) observe_string_cb, &setting_value); + ASSERT_EQ(result, NIX_OK); + ASSERT_STREQ("empty", setting_value.c_str()); +} + +TEST_F(nix_api_util_context, nix_setting_set) +{ + nix_err result = nix_setting_set(ctx, "invalid-key", "new-value"); + ASSERT_EQ(result, NIX_ERR_KEY); + + result = nix_setting_set(ctx, "setting-name", "new-value"); + ASSERT_EQ(result, NIX_OK); + + std::string setting_value; + result = nix_setting_get(ctx, "setting-name", (void *) observe_string_cb, &setting_value); + ASSERT_EQ(result, NIX_OK); + ASSERT_STREQ("new-value", setting_value.c_str()); +} + +TEST_F(nix_api_util_context, nix_err_msg) +{ + // no error + EXPECT_THROW(nix_err_msg(nullptr, ctx, NULL), nix::Error); + + // set error + nix_set_err_msg(ctx, NIX_ERR_UNKNOWN, "unknown test error"); + + // basic usage + std::string err_msg = nix_err_msg(NULL, ctx, NULL); + ASSERT_EQ(err_msg, "unknown test error"); + + // advanced usage + unsigned int sz; + err_msg = nix_err_msg(nix_c_context_create(), ctx, &sz); + ASSERT_EQ(sz, err_msg.size()); +} + +TEST_F(nix_api_util_context, nix_err_info_msg) +{ + std::string err_info; + + // no error + EXPECT_THROW(nix_err_info_msg(NULL, ctx, (void *) observe_string_cb, &err_info), nix::Error); + + try { + throw nix::Error("testing error"); + } catch (...) { + nix_context_error(ctx); + } + nix_err_info_msg(nix_c_context_create(), ctx, (void *) observe_string_cb, &err_info); + ASSERT_STREQ("testing error", err_info.c_str()); +} + +TEST_F(nix_api_util_context, nix_err_name) +{ + std::string err_name; + + // no error + EXPECT_THROW(nix_err_name(NULL, ctx, (void *) observe_string_cb, &err_name), nix::Error); + + std::string err_msg_ref; + try { + throw nix::Error("testing error"); + } catch (...) { + nix_context_error(ctx); + } + nix_err_name(nix_c_context_create(), ctx, (void *) observe_string_cb, &err_name); + ASSERT_EQ(std::string(err_name), "nix::Error"); +} + +TEST_F(nix_api_util_context, nix_err_code) +{ + ASSERT_EQ(nix_err_code(ctx), NIX_OK); + nix_set_err_msg(ctx, NIX_ERR_UNKNOWN, "unknown test error"); + ASSERT_EQ(nix_err_code(ctx), NIX_ERR_UNKNOWN); +} + +} diff --git a/tests/unit/libutil/tests.cc b/tests/unit/libutil/tests.cc index 4406fd184..d7e9edf0a 100644 --- a/tests/unit/libutil/tests.cc +++ b/tests/unit/libutil/tests.cc @@ -151,6 +151,16 @@ namespace nix { ASSERT_EQ(p1, "dir"); } + TEST(baseNameOf, trailingSlashes) { + auto p1 = baseNameOf("/dir//"); + ASSERT_EQ(p1, "dir"); + } + + TEST(baseNameOf, absoluteNothingSlashNothing) { + auto p1 = baseNameOf("//"); + ASSERT_EQ(p1, ""); + } + /* ---------------------------------------------------------------------------- * isInDir * --------------------------------------------------------------------------*/