diff --git a/nixos/doc/manual/development/running-nixos-tests-interactively.section.md b/nixos/doc/manual/development/running-nixos-tests-interactively.section.md index 66be7d5f812b..572173cb4fbf 100644 --- a/nixos/doc/manual/development/running-nixos-tests-interactively.section.md +++ b/nixos/doc/manual/development/running-nixos-tests-interactively.section.md @@ -10,6 +10,17 @@ $ ./result/bin/nixos-test-driver >>> ``` +::: {.note} +Tests using `systemd-nspawn` container machines require root privileges to run interactively, +since the driver calls `systemd-nspawn` directly to start the containers: + +``` +$ sudo ./result/bin/nixos-test-driver +[...] +>>> +``` +::: + ::: {.note} By executing the test driver in this way, the VMs executed may gain network & Internet access via their backdoor control interface, @@ -30,7 +41,7 @@ back into the test driver command line upon its completion. This allows you to inspect the state of the VMs after the test (e.g. to debug the test script). -## Shell access in interactive mode {#sec-nixos-test-shell-access} +## Shell access to VMs in interactive mode {#sec-nixos-test-shell-access} The function `.shell_interact()` grants access to a shell running inside a virtual machine. To use it, replace `` with the name of a @@ -63,7 +74,7 @@ using: Once the connection is established, you can enter commands in the socat terminal where socat is running. -## SSH Access for test machines {#sec-nixos-test-ssh-access} +## SSH Access for test VMs {#sec-nixos-test-ssh-access} An SSH-based backdoor to log into machines can be enabled with @@ -149,10 +160,10 @@ must be configured to allow these connections. ## Reuse VM state {#sec-nixos-test-reuse-vm-state} You can re-use the VM states coming from a previous run by setting the -`--keep-vm-state` flag. +`--keep-machine-state` flag. ```ShellSession -$ ./result/bin/nixos-test-driver --keep-vm-state +$ ./result/bin/nixos-test-driver --keep-machine-state ``` The machine state is stored in the `$TMPDIR/vm-state-machinename` diff --git a/nixos/doc/manual/development/running-nixos-tests.section.md b/nixos/doc/manual/development/running-nixos-tests.section.md index b8191ebd313c..dac7f97a2c4d 100644 --- a/nixos/doc/manual/development/running-nixos-tests.section.md +++ b/nixos/doc/manual/development/running-nixos-tests.section.md @@ -21,10 +21,44 @@ $ nix-store --read-log result ## System Requirements {#sec-running-nixos-tests-requirements} -NixOS tests require virtualization support. +NixOS tests using QEMU virtual machine [`nodes`](#test-opt-nodes) require virtualization support. This means that the machine must have `kvm` in its [system features](https://nixos.org/manual/nix/stable/command-ref/conf-file.html?highlight=system-features#conf-system-features) list, or `apple-virt` in case of macOS. These features are autodetected locally, but `apple-virt` is only autodetected since Nix 2.19.0. Features of **remote builders** must additionally be configured manually on the client, e.g. on NixOS with [`nix.buildMachines.*.supportedFeatures`](https://search.nixos.org/options?show=nix.buildMachines.*.supportedFeatures&sort=alpha_asc&query=nix.buildMachines) or through general [Nix configuration](https://nixos.org/manual/nix/stable/advanced-topics/distributed-builds). If you run the tests on a **macOS** machine, you also need a "remote" builder for Linux; possibly a VM. [nix-darwin](https://daiderd.com/nix-darwin/) users may enable [`nix.linux-builder.enable`](https://daiderd.com/nix-darwin/manual/index.html#opt-nix.linux-builder.enable) to launch such a VM. + +NixOS tests using `systemd-nspawn` [`containers`](#test-opt-containers) require the Nix daemon to be +configured with the following settings: + +```nix +{ + nix.settings = { + auto-allocate-uids = true; + extra-system-features = [ "uid-range" ]; + experimental-features = [ + "auto-allocate-uids" + "cgroups" + ]; + }; +} +``` + +See the documentation of the settings +[`auto-allocate-uids`](https://nix.dev/manual/nix/stable/command-ref/conf-file#conf-auto-allocate-uids), +[`uid-range`](https://nix.dev/manual/nix/stable/command-ref/conf-file.html?highlight=uid-range#conf-system-features), and +[`cgroups`](https://nix.dev/manual/nix/stable/development/experimental-features#xp-feature-cgroups) +for more information. + +If the test uses both `systemd-nspawn` [`containers`](#test-opt-containers) and QEMU virtual machine [`nodes`](#test-opt-nodes) +and requires them share a common VLAN, +`/dev/net` must be present in the sandbox. +This allows them to be bridged over a TAP interface. +To make this path available, set the following option: + +```nix +{ + nix.settings.sandbox-paths = [ "/dev/net" ]; +} +``` diff --git a/nixos/doc/manual/development/writing-nixos-tests.section.md b/nixos/doc/manual/development/writing-nixos-tests.section.md index f78d81c4fe39..7e7414bd61a9 100644 --- a/nixos/doc/manual/development/writing-nixos-tests.section.md +++ b/nixos/doc/manual/development/writing-nixos-tests.section.md @@ -4,15 +4,14 @@ A NixOS test is a module that has the following structure: ```nix { - - # One or more machines: + # QEMU virtual machines: nodes = { - machine = + vm1 = { config, pkgs, ... }: { # ... }; - machine2 = + vm2 = { config, pkgs, ... }: { # ... @@ -20,6 +19,20 @@ A NixOS test is a module that has the following structure: # … }; + # systemd-nspawn containers: + containers = { + container1 = + { config, pkgs, ... }: + { + # ... + }; + container2 = + { config, pkgs, ... }: + { + # ... + }; + }; + testScript = '' Python code… ''; @@ -27,12 +40,13 @@ A NixOS test is a module that has the following structure: ``` We refer to the whole test above as a test module, whereas the values -in [`nodes.`](#test-opt-nodes) are NixOS modules themselves. +in [`nodes.`](#test-opt-nodes) and [`containers.`](#test-opt-containers) +are NixOS modules themselves. The option [`testScript`](#test-opt-testScript) is a piece of Python code that executes the -test (described below). During the test, it will start one or more -virtual machines, the configuration of which is described by -the option [`nodes`](#test-opt-nodes). +test (described [below](#ssec-test-script)). During the test, it will start one or more +virtual machines and/or `systemd-nspawn` containers, the configuration of which is described by +the options [`nodes`](#test-opt-nodes) and [`containers`](#test-opt-containers), respectively. An example of a single-node test is [`login.nix`](https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/login.nix). @@ -42,6 +56,12 @@ when switching between consoles, and so on. An interesting multi-node test is [`nfs/simple.nix`](https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/nfs/simple.nix). It uses two client nodes to test correct locking across server crashes. +A test can contain both virtual machines and containers. +If configured to share a common VLAN, +they can reach each other over the network. +See [](https://github.com/applicative-systems/nixpkgs/blob/78100f9077ab50604ab9c9514e442bbc7ac7ca5b/nixos/tests/nixos-test-driver/containers.nix) +for an example of this and [](#sec-running-nixos-tests-requirements) for the system requirements for this scenario. + ## Calling a test {#sec-calling-nixos-tests} Tests are invoked differently depending on whether the test is part of NixOS or lives in a different project. @@ -90,13 +110,54 @@ pkgs.testers.runNixOSTest { `runNixOSTest` returns a derivation that runs the test. -## Configuring the nodes {#sec-nixos-test-nodes} +## Test machines {#ssec-nixos-test-machines} -There are a few special NixOS options for test VMs: +A NixOS test usually consists of one or more test machines. Each machine is either a +QEMU virtual machine or a `systemd-nspawn` container. -`virtualisation.memorySize` +QEMU virtual machines are defined in the +[`nodes`](#test-opt-nodes) attribute set, whereas `systemd-nspawn` containers are defined in the +[`containers`](#test-opt-containers) attribute set. -: The memory of the VM in MiB (1024×1024 bytes). +To set NixOS options for all machines in the test, use the attribute +[`defaults`](#test-opt-defaults). These options are applied to both virtual machines +and containers. You can set separate defaults for virtual machines and containers +using the attributes [`nodeDefaults`](#test-opt-nodeDefaults) and +[`containerDefaults`](#test-opt-containerDefaults), respectively. + +### Virtual machines vs. containers {#sec-nixos-test-vms-vs-containers} + +QEMU virtual machines and `systemd-nspawn` containers offer different +trade-offs which make them suitable for different use cases. + +Some advantages of containers over virtual machines are: + +- Containers share the kernel of the host system; they are + significantly faster to start up than virtual machines. +- Containers are more lightweight in terms of resource usage, which + allows running more of them in parallel on a single host. +- Containers can easily be run in virtualised environments, e.g., CI systems. +- Containers allow direct bind-mounting of host device nodes, which enables + testing of GPU code (CUDA), for example. + +Some advantages of virtual machines over containers are: + +- Virtual machines run a separate kernel, which allows testing kernel features + (kernel modules, etc.). +- Virtual machines support testing graphical applications on X11. +- Virtual machines allow testing NixOS modules that use systemd's namespacing options (such as `ProtectSystem=` or `MountAPIVFS=`). +- Virtual machines allow testing [`spcialisation`](options.html#opt-specialisation). + (Switching to a specialisation requires the creation of SUID/SGID wrappers, which is disallowed in `systemd-nspawn` within the Nix sandbox.) +- Virtual machines allow the execution of `setuid` binaries. + +Refer to the sections on [QEMU virtual machines](#ssec-nixos-test-qemu-vms) +and [systemd-nspawn containers](#ssec-nixos-test-nspawn-containers) below +for more details on configuring each type of machine. + +### Configuring test machines {#sec-nixos-test-machines-config} + +The following special NixOS option can be used to configure +machines in a NixOS test, whether they are virtual machines or containers: `virtualisation.vlans` @@ -104,6 +165,35 @@ There are a few special NixOS options for test VMs: [`nat.nix`](https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/nat.nix) for an example. +#### Configuring `systemd-nspawn` containers {#ssec-nixos-test-nspawn-containers} + +Some options are specific to `systemd-nspawn` containers: + +`virtualisation.systemd-nspawn.options` + +: A list of additional command-line options to pass to + `systemd-nspawn` when starting the container. For example, to + bind mount a directory from the host into the container, you could + use: `[ "--bind=/host/dir:/container/dir" ]`. + +For more options, see the module +[`nspawn-container`](https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/virtualisation/nspawn-container/default.nix). + +Note that the paths used in `--bind` or `--bind-ro` options have to be accessible from within the Nix sandbox. +Use the Nix option +[`sandbox-paths`](https://nix.dev/manual/nix/stable/command-ref/conf-file#conf-sandbox-paths) +and/or the module [`programs.nix-required-mounts`](#opt-programs.nix-required-mounts.enable) on the host +to add additional paths to the sandbox. + +#### Configuring QEMU virtual machines {#ssec-nixos-test-qemu-vms} + +Some options are specific to QEMU virtual machines: + +`virtualisation.memorySize` + +: The memory of the VM in MiB (1024×1024 bytes). + + `virtualisation.writableStore` : By default, the Nix store in the VM is not writable. If you enable @@ -114,13 +204,15 @@ There are a few special NixOS options for test VMs: For more options, see the module [`qemu-vm.nix`](https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/virtualisation/qemu-vm.nix). +## Writing the test script {#ssec-test-script} + The test script is a sequence of Python statements that perform various -actions, such as starting VMs, executing commands in the VMs, and so on. -Each virtual machine is represented as an object stored in the variable -`name` if this is also the identifier of the machine in the declarative -config. If you specified a node `nodes.machine`, the following example starts the -machine, waits until it has finished booting, then executes a command -and checks that the output is more-or-less correct: +actions, such as starting machines, executing commands in them, and so on. For +example, if you specified a virtual machine in `nodes.machine`, there will be +a Python variable `machine` available in the test script that represents that +virtual machine. The following example would start the machine, wait until it +has finished booting, and then execute a command and check that the output is +more-or-less correct: ```py machine.start() @@ -139,17 +231,20 @@ start_all() Under the variable `t`, all assertions from [`unittest.TestCase`](https://docs.python.org/3/library/unittest.html) are available. -If the hostname of a node contains characters that can't be used in a +If the hostname of a machine contains characters that can't be used in a Python variable name, those characters will be replaced with underscores in the variable name, so `nodes.machine-a` will be exposed to Python as `machine_a`. -## Machine objects {#ssec-machine-objects} +### Methods available on machine objects {#ssec-machine-objects} -The following methods are available on machine objects: +The following methods are available on machine objects (like `machine` in +the examples above): @PYTHON_MACHINE_METHODS@ +### Testing user units {#ssec-testing-user-units} + To test user units declared by `systemd.user.services` the optional `user` argument can be used: @@ -162,6 +257,84 @@ machine.wait_for_unit("xautolock.service", "x-session-user") This applies to `systemctl`, `get_unit_info`, `wait_for_unit`, `start_job` and `stop_job`. +### Failing tests early {#ssec-failing-tests-early} + +To fail tests early when certain invariants are no longer met (instead of waiting for the build to time out), the decorator `polling_condition` is provided. For example, if we are testing a program `foo` that should not quit after being started, we might write the following: + +```py +@polling_condition +def foo_running(): + machine.succeed("pgrep -x foo") + + +machine.succeed("foo --start") +machine.wait_until_succeeds("pgrep -x foo") + +with foo_running: + ... # Put `foo` through its paces +``` + +`polling_condition` takes the following (optional) arguments: + +`seconds_interval` + +: specifies how often the condition should be polled: + +```py +@polling_condition(seconds_interval=10) +def foo_running(): + machine.succeed("pgrep -x foo") +``` + +`description` + +: is used in the log when the condition is checked. If this is not provided, the description is pulled from the docstring of the function. These two are therefore equivalent: + +```py +@polling_condition +def foo_running(): + "check that foo is running" + machine.succeed("pgrep -x foo") +``` + +```py +@polling_condition(description="check that foo is running") +def foo_running(): + machine.succeed("pgrep -x foo") +``` + +### Adding Python packages to the test script {#ssec-python-packages-in-test-script} + +When additional Python libraries are required in the test script, they can be +added using the parameter `extraPythonPackages`. For example, you could add +`numpy` like this: + +```nix +{ + extraPythonPackages = p: [ p.numpy ]; + + nodes = { }; + + # Type checking on extra packages doesn't work yet + skipTypeCheck = true; + + testScript = '' + import numpy as np + assert str(np.zeros(4)) == "[0. 0. 0. 0.]" + ''; +} +``` + +In that case, `numpy` is chosen from the generic `python3Packages`. + +### Linting and type checking test scripts {#ssec-test-script-checks} + +Test scripts are automatically linted with +[Pyflakes](https://pypi.org/project/pyflakes/) and type-checked with +[Mypy](https://mypy.readthedocs.io/en/stable/). +If there are any linting or type checking errors, the test will +fail to evaluate. + For faster dev cycles it's also possible to disable the code-linters (this shouldn't be committed though): @@ -209,76 +382,6 @@ way: } ``` -## Failing tests early {#ssec-failing-tests-early} - -To fail tests early when certain invariants are no longer met (instead of waiting for the build to time out), the decorator `polling_condition` is provided. For example, if we are testing a program `foo` that should not quit after being started, we might write the following: - -```py -@polling_condition -def foo_running(): - machine.succeed("pgrep -x foo") - - -machine.succeed("foo --start") -machine.wait_until_succeeds("pgrep -x foo") - -with foo_running: - ... # Put `foo` through its paces -``` - -`polling_condition` takes the following (optional) arguments: - -`seconds_interval` - -: specifies how often the condition should be polled: - -```py -@polling_condition(seconds_interval=10) -def foo_running(): - machine.succeed("pgrep -x foo") -``` - -`description` - -: is used in the log when the condition is checked. If this is not provided, the description is pulled from the docstring of the function. These two are therefore equivalent: - -```py -@polling_condition -def foo_running(): - "check that foo is running" - machine.succeed("pgrep -x foo") -``` - -```py -@polling_condition(description="check that foo is running") -def foo_running(): - machine.succeed("pgrep -x foo") -``` - -## Adding Python packages to the test script {#ssec-python-packages-in-test-script} - -When additional Python libraries are required in the test script, they can be -added using the parameter `extraPythonPackages`. For example, you could add -`numpy` like this: - -```nix -{ - extraPythonPackages = p: [ p.numpy ]; - - nodes = { }; - - # Type checking on extra packages doesn't work yet - skipTypeCheck = true; - - testScript = '' - import numpy as np - assert str(np.zeros(4)) == "[0. 0. 0. 0.]" - ''; -} -``` - -In that case, `numpy` is chosen from the generic `python3Packages`. - ## Overriding a test {#sec-override-nixos-test} The NixOS test framework returns tests with multiple overriding methods. @@ -297,7 +400,7 @@ The NixOS test framework returns tests with multiple overriding methods. : Evaluates the test with additional NixOS modules and/or arguments. `module` - : A NixOS module to add to all the nodes in the test. Sets test option [`extraBaseModules`](#test-opt-extraBaseModules). + : A NixOS module to add to all the machines in the test. Sets test option [`extraBaseModules`](#test-opt-extraBaseModules). `specialArgs` : An attribute set of arguments to pass to all NixOS modules. These override the existing arguments, as well as any `_module.args.` that the modules may define. Sets test option [`node.specialArgs`](#test-opt-node.specialArgs). @@ -345,7 +448,52 @@ list-id: test-options-list source: @NIXOS_TEST_OPTIONS_JSON@ ``` -## Accessing VMs in the sandbox with SSH {#sec-test-sandbox-breakpoint} +## Debugging test machines {#sec-test-sandbox-breakpoint} + +You can set the [`enableDebugHook`](#test-opt-enableDebugHook) option to pause +a test on the first failure and have it print instructions on how to enter the +sandbox shell of the test. Suppose you have the following test module: + +```nix +{ + name = "foo"; + + nodes.machine = { }; + + enableDebugHook = true; + sshBackdoor.enable = true; + + testScript = '' + start_all() + machine.succeed("false") # this will fail + ''; +} +``` + +The test will fail with an output like this: + +``` +vm-test-run-foo> !!! Breakpoint reached, run 'sudo /nix/store/eeeee-attach/bin/attach ' +``` + +You can then enter the sandbox shell: + +``` +$ sudo /nix/store/eeeee-attach/bin/attach +bash# +``` + +There, you can attach to a [`pdb`](https://docs.python.org/3/library/pdb.html) session +to step through the Python test script: + +``` +bash# telnet 127.0.0.1 4444 +pdb$ +``` + +Note that it is also possible to set breakpoints in the test script using `debug.breakpoint()`. + +### SSH access to test VMs {#sec-test-vm-ssh-access} ::: {.note} For debugging with SSH access into the machines, it's recommended to try using @@ -356,24 +504,15 @@ This feature is mostly intended to debug flaky test failures that aren't reproducible elsewhere. ::: -As explained in [](#sec-nixos-test-ssh-access), it's possible to configure an -SSH backdoor based on AF_VSOCK. This can be used to SSH into a VM of a running -build in a sandbox. -This can be done when something in the test fails, e.g. +If you set the [`sshBackdoor.enable`](#test-opt-sshBackdoor.enable) option, +QEMU virtual machines will open an SSH backdoor based on AF_VSOCK +(see [](#sec-nixos-test-ssh-access)). +Once you are in the sandbox shell, you can access the VMs (for example, `machine`) +with SSH over vsock: -```nix -{ - nodes.machine = { }; - - sshBackdoor.enable = true; - enableDebugHook = true; - - testScript = '' - start_all() - machine.succeed("false") # this will fail - ''; -} +``` +bash# ssh -F ./ssh_config vsock/3 ``` For the AF_VSOCK feature to work, `/dev/vhost-vsock` is needed in the sandbox @@ -383,24 +522,24 @@ which can be done with e.g. nix-build -A nixosTests.foo --option sandbox-paths /dev/vhost-vsock ``` -This will halt the test execution on a test-failure and print instructions -on how to enter the sandbox shell of the VM test. Inside, one can log into -e.g. `machine` with - -``` -ssh -F ./ssh_config vsock/3 -``` - As described in [](#sec-nixos-test-ssh-access), the numbers for vsock start at `3` instead of `1`. So the first VM in the network (sorted alphabetically) can be accessed with `vsock/3`. -Alternatively, it's possible to explicitly set a breakpoint with -`debug.breakpoint()`. This also has the benefit, that one can step through -`testScript` with `pdb` like this: +### SSH access to test containers {#sec-test-container-ssh-access} + +If you set the [`sshBackdoor.enable`](#test-opt-sshBackdoor.enable) option, +each `systemd-nspawn` container will open an SSH backdoor. +Once the container starts, +it will print instructions on how to log into the container via SSH. +If the test fails, +attach to the sandbox as described above, +and then use the provided SSH command to log into the container. +For example: ``` -$ sudo /nix/store/eeeee-attach -bash# telnet 127.0.0.1 4444 -pdb$ … +$ sudo /nix/store/eeeee-attach +bash# ssh -o User=root -o ProxyCommand="socat - UNIX-CLIENT:/run/systemd/nspawn/unix-export/machine/ssh" bash +[root@machine:~]# hostname +machine ``` diff --git a/nixos/doc/manual/redirects.json b/nixos/doc/manual/redirects.json index be546b0ce617..7d8c3382175d 100644 --- a/nixos/doc/manual/redirects.json +++ b/nixos/doc/manual/redirects.json @@ -88,6 +88,9 @@ "module-virtualisation-xen-introduction": [ "index.html#module-virtualisation-xen-introduction" ], + "sec-nixos-test-vms-vs-containers": [ + "index.html#sec-nixos-test-vms-vs-containers" + ], "sec-override-nixos-test": [ "index.html#sec-override-nixos-test" ], @@ -100,6 +103,51 @@ "sec-wireless-imperative": [ "index.html#sec-wireless-imperative" ], + "sec-test-container-ssh-access": [ + "index.html#sec-test-container-ssh-access" + ], + "sec-test-vm-ssh-access": [ + "index.html#sec-test-vm-ssh-access" + ], + "ssec-all-machine-objects": [ + "index.html#ssec-all-machine-objects" + ], + "ssec-nixos-test-machines": [ + "index.html#ssec-nixos-test-machines" + ], + "ssec-nixos-test-nspawn-containers": [ + "index.html#ssec-nixos-test-nspawn-containers" + ], + "ssec-nixos-test-qemu-vms": [ + "index.html#ssec-nixos-test-qemu-vms" + ], + "ssec-nspawn-machine-objects": [ + "index.html#ssec-nspawn-machine-objects" + ], + "ssec-qemu-machine-objects": [ + "index.html#ssec-qemu-machine-objects" + ], + "ssec-test-script": [ + "index.html#ssec-test-script" + ], + "ssec-test-script-checks": [ + "index.html#ssec-test-script-checks" + ], + "ssec-testing-user-units": [ + "index.html#ssec-testing-user-units" + ], + "test-opt-containerDefaults": [ + "index.html#test-opt-containerDefaults" + ], + "test-opt-containers": [ + "index.html#test-opt-containers" + ], + "test-opt-extraBaseModules": [ + "index.html#test-opt-extraBaseModules" + ], + "test-opt-nodeDefaults": [ + "index.html#test-opt-nodeDefaults" + ], "test-opt-rawTestDerivationArg": [ "index.html#test-opt-rawTestDerivationArg" ], @@ -2037,7 +2085,8 @@ "sec-call-nixos-test-outside-nixos": [ "index.html#sec-call-nixos-test-outside-nixos" ], - "sec-nixos-test-nodes": [ + "sec-nixos-test-machines-config": [ + "index.html#sec-nixos-test-machines-config", "index.html#sec-nixos-test-nodes" ], "ssec-machine-objects": [ @@ -2070,8 +2119,8 @@ "test-opt-enableOCR": [ "index.html#test-opt-enableOCR" ], - "test-opt-extraBaseModules": [ - "index.html#test-opt-extraBaseModules" + "test-opt-extraBaseNodeModules": [ + "index.html#test-opt-extraBaseNodeModules" ], "test-opt-extraDriverArgs": [ "index.html#test-opt-extraDriverArgs" diff --git a/nixos/doc/manual/release-notes/rl-2605.section.md b/nixos/doc/manual/release-notes/rl-2605.section.md index 8c27e3256ad3..4b8727bc1830 100644 --- a/nixos/doc/manual/release-notes/rl-2605.section.md +++ b/nixos/doc/manual/release-notes/rl-2605.section.md @@ -14,6 +14,8 @@ designed to run on affordable, low-power devices. Available as [services.meshtasticd] (#opt-services.meshtasticd.enable). +- [Goupile](https://goupile.org/en), an open-source design tool for secure forms including Clinical Report Forms (eCRF). Available as [services.goupile](#opt-services.goupile.enable). + - [knot-resolver](https://www.knot-resolver.cz/) in version 6. Available as `services.knot-resolver`. A module for knot-resolver 5 was already available as `services.kresd`. - [ImmichFrame](https://immichframe.dev/), display your photos from Immich as a digital photo frame. Available as `services.immichframe`. diff --git a/nixos/lib/test-driver/default.nix b/nixos/lib/test-driver/default.nix index ed8eb2c8c771..72aa6b11bfe5 100644 --- a/nixos/lib/test-driver/default.nix +++ b/nixos/lib/test-driver/default.nix @@ -19,9 +19,12 @@ qemu_test, setuptools, socat, + systemd, tesseract4, + util-linux, vde2, + enableNspawn ? false, enableOCR ? false, extraPythonPackages ? (_: [ ]), }: @@ -51,8 +54,12 @@ buildPythonApplication { netpbm qemu_pkg socat + util-linux vde2 ] + ++ lib.optionals enableNspawn [ + systemd + ] ++ lib.optionals enableOCR [ imagemagick_light tesseract4 diff --git a/nixos/lib/test-driver/src/extract-docstrings.py b/nixos/lib/test-driver/src/extract-docstrings.py index 64850ca711f3..4daedd6b9278 100644 --- a/nixos/lib/test-driver/src/extract-docstrings.py +++ b/nixos/lib/test-driver/src/extract-docstrings.py @@ -1,6 +1,7 @@ import ast import sys from pathlib import Path +from typing import List """ This program takes all the Machine class methods and prints its methods in @@ -41,6 +42,38 @@ some_function(param1, param2) """ +def function_docstrings(functions: List[ast.FunctionDef]) -> str | None: + """Extracts docstrings from a list of function definitions.""" + documented_functions = [f for f in functions if ast.get_docstring(f) is not None] + + if not documented_functions: + return None + + docstrings = [] + for function in documented_functions: + docstr = ast.get_docstring(function) + assert docstr is not None + + args = ", ".join(a.arg for a in function.args.args[1:]) + args = f"({args})" + + docstr = "\n".join(f" {line}" for line in docstr.strip().splitlines()) + + docstrings.append(f"{function.name}{args}\n\n:{docstr[1:]}\n") + return "\n".join(docstrings) + +def machine_methods(class_name: str, class_definitions: List[ast.ClassDef]) -> List[ast.FunctionDef]: + """Given a class name and a list of class definitions, returns the list of function definitions + for the class matching the given name. + """ + machine_class = next(filter(lambda x: x.name == class_name, class_definitions)) + assert machine_class is not None + + function_definitions = [ + node for node in machine_class.body if isinstance(node, ast.FunctionDef) + ] + function_definitions.sort(key=lambda x: x.name) + return function_definitions def main() -> None: if len(sys.argv) != 2: @@ -49,26 +82,27 @@ def main() -> None: module = ast.parse(Path(sys.argv[1]).read_text()) - class_definitions = (node for node in module.body if isinstance(node, ast.ClassDef)) + class_definitions = [node for node in module.body if isinstance(node, ast.ClassDef)] - machine_class = next(filter(lambda x: x.name == "Machine", class_definitions)) - assert machine_class is not None + base_machine_methods = machine_methods("BaseMachine", class_definitions) + base_method_names = {method.name for method in base_machine_methods} - function_definitions = [ - node for node in machine_class.body if isinstance(node, ast.FunctionDef) + qemu_machine_methods = [ + method for method in machine_methods("QemuMachine", class_definitions) + if method.name not in base_method_names ] - function_definitions.sort(key=lambda x: x.name) - for function in function_definitions: - docstr = ast.get_docstring(function) - if docstr is not None: - args = ", ".join(a.arg for a in function.args.args[1:]) - args = f"({args})" - - docstr = "\n".join(f" {line}" for line in docstr.strip().splitlines()) - - print(f"{function.name}{args}\n\n:{docstr[1:]}\n") + nspawn_machine_methods = [ + method for method in machine_methods("NspawnMachine", class_definitions) + if method.name not in base_method_names + ] + print("#### Generic machine objects {#ssec-all-machine-objects} \n") + print(function_docstrings(base_machine_methods)) + print("#### QEMU VM objects {#ssec-qemu-machine-objects}\n") + print(function_docstrings(qemu_machine_methods) or "No methods specific to QEMU virtual machines.") + print("#### `systemd-nspawn` container objects {#ssec-nspawn-machine-objects}\n") + print(function_docstrings(nspawn_machine_methods) or "No methods specific to `systemd-nspawn` containers.") if __name__ == "__main__": main() diff --git a/nixos/lib/test-driver/src/test_driver/__init__.py b/nixos/lib/test-driver/src/test_driver/__init__.py index 422cf3917233..0f42f2842c77 100644 --- a/nixos/lib/test-driver/src/test_driver/__init__.py +++ b/nixos/lib/test-driver/src/test_driver/__init__.py @@ -1,6 +1,8 @@ import argparse import os +import sys import time +import warnings from pathlib import Path import ptpython.ipython @@ -16,7 +18,7 @@ from test_driver.logger import ( class EnvDefault(argparse.Action): - """An argpars Action that takes values from the specified + """An argparse Action that takes values from the specified environment variable as the flags default value. """ @@ -55,9 +57,15 @@ def writeable_dir(arg: str) -> Path: def main() -> None: arg_parser = argparse.ArgumentParser(prog="nixos-test-driver") arg_parser.add_argument( - "-K", "--keep-vm-state", - help="re-use a VM state coming from a previous run", + help=argparse.SUPPRESS, + dest="keep_machine_state", + action="store_true", + ) + arg_parser.add_argument( + "-K", + "--keep-machine-state", + help="re-use a machine state coming from a previous run", action="store_true", ) arg_parser.add_argument( @@ -71,13 +79,37 @@ def main() -> None: help="Enable interactive debugging breakpoints for sandboxed runs", ) arg_parser.add_argument( - "--start-scripts", - metavar="START-SCRIPT", + "--vm-names", + metavar="VM-NAME", action=EnvDefault, - envvar="startScripts", + envvar="vmNames", + nargs="*", + help="names of participating virtual machines", + ) + arg_parser.add_argument( + "--vm-start-scripts", + metavar="VM-START-SCRIPT", + action=EnvDefault, + envvar="vmStartScripts", nargs="*", help="start scripts for participating virtual machines", ) + arg_parser.add_argument( + "--container-names", + metavar="CONTAINER-NAME", + action=EnvDefault, + envvar="containerNames", + nargs="*", + help="names of participating containers", + ) + arg_parser.add_argument( + "--container-start-scripts", + metavar="CONTAINER-START-SCRIPT", + action=EnvDefault, + envvar="containerStartScripts", + nargs="*", + help="start scripts for participating containers", + ) arg_parser.add_argument( "--vlans", metavar="VLAN", @@ -97,8 +129,8 @@ def main() -> None: arg_parser.add_argument( "-o", "--output_directory", - help="""The path to the directory where outputs copied from the VM will be placed. - By e.g. Machine.copy_from_vm or Machine.screenshot""", + help="""The path to the directory where outputs copied from the machine will be placed. + By e.g. NspawnMachine.copy_from_machine or QemuMachine.screenshot""", default=Path.cwd(), type=writeable_dir, ) @@ -122,6 +154,12 @@ def main() -> None: args = arg_parser.parse_args() + if "--keep-vm-state" in sys.argv: + warnings.warn( + "The flag '--keep-vm-state' is deprecated. Use '--keep-machine-state' instead.", + DeprecationWarning, + ) + output_directory = args.output_directory.resolve() logger = CompositeLogger([TerminalLogger()]) @@ -131,21 +169,33 @@ def main() -> None: if args.junit_xml: logger.add_logger(JunitXMLLogger(output_directory / args.junit_xml)) - if not args.keep_vm_state: - logger.info("Machine state will be reset. To keep it, pass --keep-vm-state") + if not args.keep_machine_state: + logger.info( + "Machine state will be reset. To keep it, pass --keep-machine-state" + ) debugger: DebugAbstract = DebugNop() if args.debug_hook_attach is not None: debugger = Debug(logger, args.debug_hook_attach) + assert len(args.vm_names) == len(args.vm_start_scripts), ( + f"the number of vm names and vm start scripts must be the same: {args.vm_names} vs. {args.vm_start_scripts}" + ) + assert len(args.container_names) == len(args.container_start_scripts), ( + f"the number of container names and container start scripts must be the same: {args.container_names} vs. {args.container_start_scripts}" + ) + with Driver( - args.start_scripts, - args.vlans, - args.testscript.read_text(), - output_directory, - logger, - args.keep_vm_state, - args.global_timeout, + vm_names=args.vm_names, + vm_start_scripts=args.vm_start_scripts, + container_names=args.container_names, + container_start_scripts=args.container_start_scripts, + vlans=args.vlans, + tests=args.testscript.read_text(), + out_dir=output_directory, + logger=logger, + keep_machine_state=args.keep_machine_state, + global_timeout=args.global_timeout, debug=debugger, ) as driver: if offset := args.dump_vsocks: @@ -170,7 +220,16 @@ def generate_driver_symbols() -> None: in user's test scripts. That list is then used by pyflakes to lint those scripts. """ - d = Driver([], [], "", Path(), CompositeLogger([])) + d = Driver( + vm_names=[], + vm_start_scripts=[], + container_names=[], + container_start_scripts=[], + vlans=[], + tests="", + out_dir=Path(), + logger=CompositeLogger([]), + ) test_symbols = d.test_symbols() with open("driver-symbols", "w") as fp: fp.write(",".join(test_symbols.keys())) diff --git a/nixos/lib/test-driver/src/test_driver/driver.py b/nixos/lib/test-driver/src/test_driver/driver.py index c4f268404cbc..8cbcb052be49 100644 --- a/nixos/lib/test-driver/src/test_driver/driver.py +++ b/nixos/lib/test-driver/src/test_driver/driver.py @@ -1,6 +1,7 @@ import os import re import signal +import subprocess import sys import tempfile import threading @@ -16,7 +17,12 @@ from colorama import Style from test_driver.debug import DebugAbstract, DebugNop from test_driver.errors import MachineError, RequestedAssertionFailed from test_driver.logger import AbstractLogger -from test_driver.machine import Machine, NixStartScript, retry +from test_driver.machine import ( + BaseMachine, + NspawnMachine, + QemuMachine, + retry, +) from test_driver.polling_condition import PollingCondition from test_driver.vlan import VLan @@ -63,7 +69,8 @@ class Driver: tests: str vlans: list[VLan] - machines: list[Machine] + machines_qemu: list[QemuMachine] + machines_nspawn: list[NspawnMachine] polling_conditions: list[PollingCondition] global_timeout: int race_timer: threading.Timer @@ -72,12 +79,15 @@ class Driver: def __init__( self, - start_scripts: list[str], + vm_names: list[str], + vm_start_scripts: list[str], + container_names: list[str], + container_start_scripts: list[str], vlans: list[int], tests: str, out_dir: Path, logger: AbstractLogger, - keep_vm_state: bool = False, + keep_machine_state: bool = False, global_timeout: int = 24 * 60 * 60 * 7, debug: DebugAbstract = DebugNop(), ): @@ -94,25 +104,95 @@ class Driver: vlans = list(set(vlans)) self.vlans = [VLan(nr, tmp_dir, self.logger) for nr in vlans] - def cmd(scripts: list[str]) -> Iterator[NixStartScript]: - for s in scripts: - yield NixStartScript(s) - self.polling_conditions = [] - self.machines = [ - Machine( - start_command=cmd, - keep_vm_state=keep_vm_state, - name=cmd.machine_name, + self.machines_qemu = [ + QemuMachine( + name=name, + start_command=vm_start_script, + keep_machine_state=keep_machine_state, tmp_dir=tmp_dir, callbacks=[self.check_polling_conditions], out_dir=self.out_dir, logger=self.logger, ) - for cmd in cmd(start_scripts) + for name, vm_start_script in zip(vm_names, vm_start_scripts) ] + if len(container_start_scripts) > 0: + self._init_nspawn_environment() + + self.machines_nspawn = [ + NspawnMachine( + name=name, + start_command=container_start_script, + tmp_dir=tmp_dir, + logger=self.logger, + keep_machine_state=keep_machine_state, + callbacks=[self.check_polling_conditions], + out_dir=self.out_dir, + ) + for name, container_start_script in zip( + container_names, + container_start_scripts, + ) + ] + + def _init_nspawn_environment(self) -> None: + assert os.geteuid() == 0, ( + f"systemd-nspawn requires root to work. You are {os.geteuid()}" + ) + + # set up prerequisites for systemd-nspawn containers. + # these are not guaranteed to be set up in the Nix sandbox. + # if running interactively as root, these will already be set up. + + # check if /run is writable by root + if not os.access("/run", os.W_OK): + Path("/run").mkdir(parents=True, exist_ok=True) + subprocess.run(["mount", "-t", "tmpfs", "none", "/run"], check=True) + Path("/run/netns").mkdir(parents=True, exist_ok=True) + + # check if /var/run is a symlink to /run + if not (os.path.exists("/var/run") and os.path.samefile("/var/run", "/run")): + Path("/var").mkdir(parents=True, exist_ok=True) + subprocess.run(["ln", "-s", "/run", "/var/run"], check=True) + + # check if /sys/fs/cgroup is mounted as cgroup2 + with open("/proc/mounts", encoding="utf-8") as mounts: + for line in mounts: + parts = line.split() + if len(parts) >= 3 and parts[1] == "/sys/fs/cgroup": + if parts[2] == "cgroup2": + break + else: + Path("/sys/fs/cgroup").mkdir(parents=True, exist_ok=True) + subprocess.run( + ["mount", "-t", "cgroup2", "none", "/sys/fs/cgroup"], check=True + ) + + # systemd-nspawn requires that /etc/os-release exists + # It supports SYSTEMD_NSPAWN_CHECK_OS_RELEASE=0, but that + # would try to "fix" it by bind mounting, which is worse. + if not os.path.isfile("/etc/os-release"): + subprocess.run(["touch", "/etc/os-release"], check=True) + + # ensure /etc/machine-id exists and is non-empty + if ( + not os.path.isfile("/etc/machine-id") + or os.path.getsize("/etc/machine-id") == 0 + ): + subprocess.run( + ["systemd-machine-id-setup"], check=True + ) # set up /etc/machine-id + + @property + def machines(self) -> list[QemuMachine | NspawnMachine]: + machines = self.machines_qemu + self.machines_nspawn + # Sort the machines by name for consistency with `nodesAndContainers` in . + machines.sort(key=lambda machine: machine.name) + return machines + def __enter__(self) -> "Driver": return self @@ -148,7 +228,8 @@ class Driver: general_symbols = dict( start_all=self.start_all, test_script=self.test_script, - machines=self.machines, + machines_qemu=self.machines_qemu, + machines_nspawn=self.machines_nspawn, vlans=self.vlans, driver=self, log=self.logger, @@ -161,7 +242,7 @@ class Driver: serial_stdout_off=self.serial_stdout_off, serial_stdout_on=self.serial_stdout_on, polling_condition=self.polling_condition, - Machine=Machine, # for typing + BaseMachine=BaseMachine, # for typing t=AssertionTester(), debug=self.debug, ) @@ -186,14 +267,14 @@ class Driver: def dump_machine_ssh(self, offset: int) -> None: print("SSH backdoor enabled, the machines can be accessed like this:") print( - f"{Style.BRIGHT}Note:{Style.RESET_ALL} this requires {Style.BRIGHT}systemd-ssh-proxy(1){Style.RESET_ALL} to be enabled (default on NixOS 25.05 and newer)." + f"{Style.BRIGHT}Note:{Style.RESET_ALL} vsocks require {Style.BRIGHT}systemd-ssh-proxy(1){Style.RESET_ALL} to be enabled (default on NixOS 25.05 and newer)." ) - names = [machine.name for machine in self.machines] - longest_name = len(max(names, key=len)) - for num, name in enumerate(names, start=offset + 1): + longest_name = len(max((machine.name for machine in self.machines), key=len)) + for index, machine in enumerate(self.machines, start=offset + 1): + name = machine.name spaces = " " * (longest_name - len(name) + 2) print( - f" {name}:{spaces}{Style.BRIGHT}ssh -o User=root vsock/{num}{Style.RESET_ALL}" + f" {name}:{spaces}{Style.BRIGHT}{machine.ssh_backdoor_command(index)}{Style.RESET_ALL}" ) def test_script(self) -> None: @@ -252,8 +333,16 @@ class Driver: def start_all(self) -> None: """Start all machines""" with self.logger.nested("start all VMs"): + threads = [] for machine in self.machines: - machine.start() + # Create a thread for each machine's start method + t = threading.Thread(target=machine.start, name=f"start-{machine.name}") + threads.append(t) + t.start() + + # Wait for all startup threads to complete before proceeding + for t in threads: + t.join() def join_all(self) -> None: """Wait for all machines to shut down""" @@ -279,19 +368,19 @@ class Driver: start_command: str, *, name: str | None = None, - keep_vm_state: bool = False, - ) -> Machine: + keep_machine_state: bool = False, + ) -> BaseMachine: + """ + Create a `QemuMachine`. This currently only supports qemu "nodes", not containers. + """ tmp_dir = get_tmp_dir() - cmd = NixStartScript(start_command) - name = name or cmd.machine_name - - return Machine( + return QemuMachine( tmp_dir=tmp_dir, out_dir=self.out_dir, - start_command=cmd, + start_command=start_command, name=name, - keep_vm_state=keep_vm_state, + keep_machine_state=keep_machine_state, logger=self.logger, ) diff --git a/nixos/lib/test-driver/src/test_driver/machine/__init__.py b/nixos/lib/test-driver/src/test_driver/machine/__init__.py index f722d36ae40e..365826dc22ff 100644 --- a/nixos/lib/test-driver/src/test_driver/machine/__init__.py +++ b/nixos/lib/test-driver/src/test_driver/machine/__init__.py @@ -13,8 +13,11 @@ import sys import tempfile import threading import time +import warnings +from abc import ABC, abstractmethod from collections.abc import Callable, Generator from contextlib import _GeneratorContextManager, contextmanager, nullcontext +from functools import cached_property from pathlib import Path from queue import Queue from typing import Any @@ -114,15 +117,30 @@ def retry(fn: Callable, timeout_seconds: int = 900) -> None: ) -class StartCommand: - """The Base Start Command knows how to append the necessary +class QemuStartCommand: + """This class knows how to append the necessary runtime qemu options as determined by a particular test driver - run. Any such start command is expected to happily receive and - append additional qemu args. + run. """ _cmd: str + def __init__(self, script: str): + self._cmd = script + + @property + def machine_name(self) -> str: + """A start script from nixos/modules/virtualiation/qemu-vm.nix. + These Nix commands have the particular characteristic that the + machine name can be extracted out of them via a regex match. + (Admittedly a _very_ implicit contract, TODO fix this eventually.) + """ + match = re.search("run-(.+)-vm$", self._cmd) + name = "machine" + if match: + name = match.group(1) + return name + def cmd( self, monitor_socket_path: Path, @@ -198,103 +216,39 @@ class StartCommand: ) -class NixStartScript(StartCommand): - """A start script from nixos/modules/virtualiation/qemu-vm.nix. - These Nix commands have the particular characteristic that the - machine name can be extracted out of them via a regex match. - (Admittedly a _very_ implicit contract, evtl. TODO fix) - """ - - def __init__(self, script: str): - self._cmd = script - - @property - def machine_name(self) -> str: - match = re.search("run-(.+)-vm$", self._cmd) - name = "machine" - if match: - name = match.group(1) - return name - - -class Machine: - """A handle to the machine with this name, that also knows how to manage - the machine lifecycle with the help of a start script / command.""" - +class BaseMachine(ABC): name: str - out_dir: Path - tmp_dir: Path - shared_dir: Path - state_dir: Path - monitor_path: Path - qmp_path: Path - shell_path: Path - - start_command: StartCommand - keep_vm_state: bool - - process: subprocess.Popen | None - pid: int | None - monitor: socket.socket | None - qmp_client: QMPSession | None - shell: socket.socket | None - serial_thread: threading.Thread | None - - booted: bool - connected: bool - # Store last serial console lines for use - # of wait_for_console_text - last_lines: Queue = Queue() - # Store all console output for full log retrieval - full_console_log: list[str] callbacks: list[Callable] + tmp_dir: Path + keep_machine_state: bool def __repr__(self) -> str: - return f"" + return f"<{self.__class__.__name__} '{self.name}'>" def __init__( self, out_dir: Path, - tmp_dir: Path, - start_command: StartCommand, + name: str, logger: AbstractLogger, - name: str = "machine", - keep_vm_state: bool = False, - callbacks: list[Callable] | None = None, + tmp_dir: Path, + callbacks: list[Callable] | None, + keep_machine_state: bool, ) -> None: self.out_dir = out_dir - self.tmp_dir = tmp_dir - self.keep_vm_state = keep_vm_state self.name = name - self.start_command = start_command - self.callbacks = callbacks if callbacks is not None else [] self.logger = logger - self.full_console_log = [] + self.callbacks = callbacks if callbacks is not None else [] + self.tmp_dir = tmp_dir - # set up directories - self.shared_dir = self.tmp_dir / "shared-xchg" - self.shared_dir.mkdir(mode=0o700, exist_ok=True) + self.keep_machine_state = keep_machine_state self.state_dir = self.tmp_dir / f"vm-state-{self.name}" - self.monitor_path = self.state_dir / "monitor" - self.qmp_path = self.state_dir / "qmp" - self.shell_path = self.state_dir / "shell" - if (not self.keep_vm_state) and self.state_dir.exists(): + if (not self.keep_machine_state) and self.state_dir.exists(): self.cleanup_statedir() self.state_dir.mkdir(mode=0o700, exist_ok=True) - self.process = None - self.pid = None - self.monitor = None - self.qmp_client = None - self.shell = None - self.serial_thread = None - - self.booted = False - self.connected = False - - def is_up(self) -> bool: - return self.booted and self.connected + self.shared_dir = self.tmp_dir / "shared-xchg" + self.shared_dir.mkdir(mode=0o700, exist_ok=True) def log(self, msg: str) -> None: """ @@ -313,28 +267,51 @@ class Machine: my_attrs.update(attrs) return self.logger.nested(msg, my_attrs) - def wait_for_monitor_prompt(self) -> str: - assert self.monitor is not None - answer = "" - while True: - undecoded_answer = self.monitor.recv(1024) - if not undecoded_answer: - break - answer += undecoded_answer.decode() - if answer.endswith("(qemu) "): - break - return answer + @abstractmethod + def is_up(self) -> bool: ... - def send_monitor_command(self, command: str) -> str: + @abstractmethod + def start(self) -> None: """ - Send a command to the QEMU monitor. This allows attaching - virtual USB disks to a running machine, among other things. + Start the machine. This method is asynchronous --- it does + not wait for the machine to finish booting. """ - self.run_callbacks() - message = f"{command}\n".encode() - assert self.monitor is not None - self.monitor.send(message) - return self.wait_for_monitor_prompt() + ... + + @abstractmethod + def wait_for_shutdown(self) -> None: + """Wait for the machine to power off. This does *not* initiate a shutdown; + that's usually done via `shutdown()`. + """ + ... + + @abstractmethod + def shutdown(self) -> None: + """Shutdown the machine gracefully, waiting for it to exit.""" + ... + + def systemctl(self, q: str, user: str | None = None) -> tuple[int, str]: + """ + Runs `systemctl` commands with optional support for + `systemctl --user` + + ```py + # run `systemctl list-jobs --no-pager` + machine.systemctl("list-jobs --no-pager") + + # spawn a shell for `any-user` and run + # `systemctl --user list-jobs --no-pager` + machine.systemctl("list-jobs --no-pager", "any-user") + ``` + """ + if user is not None: + q = q.replace("'", "\\'") + return self.execute( + f"su -l {user} --shell /bin/sh -c " + "$'XDG_RUNTIME_DIR=/run/user/`id -u` " + f"systemctl --user {q}'" + ) + return self.execute(f"systemctl {q}") def wait_for_unit( self, unit: str, user: str | None = None, timeout: int = 900 @@ -424,29 +401,6 @@ class Machine: assert match[1] == property, invalid_output_message return match[2] - def systemctl(self, q: str, user: str | None = None) -> tuple[int, str]: - """ - Runs `systemctl` commands with optional support for - `systemctl --user` - - ```py - # run `systemctl list-jobs --no-pager` - machine.systemctl("list-jobs --no-pager") - - # spawn a shell for `any-user` and run - # `systemctl --user list-jobs --no-pager` - machine.systemctl("list-jobs --no-pager", "any-user") - ``` - """ - if user is not None: - q = q.replace("'", "\\'") - return self.execute( - f"su -l {user} --shell /bin/sh -c " - "$'XDG_RUNTIME_DIR=/run/user/`id -u` " - f"systemctl --user {q}'" - ) - return self.execute(f"systemctl {q}") - def require_unit_state(self, unit: str, require_state: str = "active") -> None: """ Assert that the current state of a unit has a specific value. The default state is "active". @@ -462,21 +416,154 @@ class Machine: f"'{require_state}' but it is in state '{state}'" ) - def _next_newline_closed_block_from_shell(self) -> str: - assert self.shell - output_buffer = [] - while True: - # This receives up to 4096 bytes from the socket - chunk = self.shell.recv(4096) - if not chunk: - # Probably a broken pipe, return the output we have - break + def succeed(self, *commands: str, timeout: int | None = None) -> str: + """ + Execute a shell command, raising an exception if the exit status is + not zero, otherwise returning the standard output. Similar to `execute`, + except that the timeout is `None` by default. See `execute` for details on + command execution. + """ + output = "" + for command in commands: + with self.nested(f"must succeed: {command}"): + (status, out) = self.execute(command, timeout=timeout) + if status != 0: + self.log(f"output: {out}") + raise RequestedAssertionFailed( + f"command `{command}` failed (exit code {status})" + ) + output += out + return output - decoded = chunk.decode() - output_buffer += [decoded] - if decoded[-1] == "\n": - break - return "".join(output_buffer) + def fail(self, *commands: str, timeout: int | None = None) -> str: + """ + Like `succeed`, but raising an exception if the command returns a zero + status. + """ + output = "" + for command in commands: + with self.nested(f"must fail: {command}"): + (status, out) = self.execute(command, timeout=timeout) + if status == 0: + raise RequestedAssertionFailed( + f"command `{command}` unexpectedly succeeded" + ) + output += out + return output + + def wait_until_succeeds(self, command: str, timeout: int = 900) -> str: + """ + Repeat a shell command with 1-second intervals until it succeeds. + Has a default timeout of 900 seconds which can be modified, e.g. + `wait_until_succeeds(cmd, timeout=10)`. See `execute` for details on + command execution. + Throws an exception on timeout. + """ + output = "" + + def check_success(_last_try: bool) -> bool: + nonlocal output + status, output = self.execute(command, timeout=timeout) + return status == 0 + + with self.nested(f"waiting for success: {command}"): + retry(check_success, timeout) + return output + + def wait_until_fails(self, command: str, timeout: int = 900) -> str: + """ + Like `wait_until_succeeds`, but repeating the command until it fails. + """ + output = "" + + def check_failure(_last_try: bool) -> bool: + nonlocal output + status, output = self.execute(command, timeout=timeout) + return status != 0 + + with self.nested(f"waiting for failure: {command}"): + retry(check_failure, timeout) + return output + + def sleep(self, secs: int) -> None: + # We want to sleep in *guest* time, not *host* time. + self.succeed(f"sleep {secs}") + + def wait_for_file(self, filename: str, timeout: int = 900) -> None: + """ + Waits until the file exists in the machine's file system. + """ + + def check_file(_last_try: bool) -> bool: + status, _ = self.execute(f"test -e {filename}") + return status == 0 + + with self.nested(f"waiting for file '{filename}'"): + retry(check_file, timeout) + + def wait_for_open_port( + self, port: int, addr: str = "localhost", timeout: int = 900 + ) -> None: + """ + Wait until a process is listening on the given TCP port and IP address + (default `localhost`). + """ + + def port_is_open(_last_try: bool) -> bool: + status, _ = self.execute(f"nc -z {addr} {port}") + return status == 0 + + with self.nested(f"waiting for TCP port {port} on {addr}"): + retry(port_is_open, timeout) + + def wait_for_open_unix_socket( + self, addr: str, is_datagram: bool = False, timeout: int = 900 + ) -> None: + """ + Wait until a process is listening on the given UNIX-domain socket + (default to a UNIX-domain stream socket). + """ + + nc_flags = [ + "-z", + "-uU" if is_datagram else "-U", + ] + + def socket_is_open(_last_try: bool) -> bool: + status, _ = self.execute(f"nc {' '.join(nc_flags)} {addr}") + return status == 0 + + with self.nested( + f"waiting for UNIX-domain {'datagram' if is_datagram else 'stream'} on '{addr}'" + ): + retry(socket_is_open, timeout) + + def wait_for_closed_port( + self, port: int, addr: str = "localhost", timeout: int = 900 + ) -> None: + """ + Wait until nobody is listening on the given TCP port and IP address + (default `localhost`). + """ + + def port_is_closed(_last_try: bool) -> bool: + status, _ = self.execute(f"nc -z {addr} {port}") + return status != 0 + + with self.nested(f"waiting for TCP port {port} on {addr} to be closed"): + retry(port_is_closed, timeout) + + def start_job(self, jobname: str, user: str | None = None) -> tuple[int, str]: + """ + Start systemd service. + """ + return self.systemctl(f"start {jobname}", user) + + def stop_job(self, jobname: str, user: str | None = None) -> tuple[int, str]: + """ + Stop systemd service. + """ + return self.systemctl(f"stop {jobname}", user) def execute( self, @@ -510,7 +597,7 @@ class Machine: Takes an optional parameter `check_return` that defaults to `True`. Setting this parameter to `False` will not check for the return code and return -1 instead. This can be used for commands that shut down - the VM and would therefore break the pipe that would be used for + the machine and would therefore break the pipe that would be used for retrieving the return code. A timeout for the command can be specified (in seconds) using the optional @@ -518,6 +605,253 @@ class Machine: `execute(cmd, timeout=None)`. The default is 900 seconds. """ self.run_callbacks() + return self._execute( + command=command, + check_return=check_return, + check_output=check_output, + timeout=timeout, + ) + + @abstractmethod + def _execute( + self, + command: str, + check_return: bool = True, + check_output: bool = True, + timeout: int | None = 900, + ) -> tuple[int, str]: ... + + def run_callbacks(self) -> None: + for callback in self.callbacks: + callback() + + def cleanup_statedir(self) -> None: + shutil.rmtree(self.state_dir) + self.log(f"deleting machine state directory {self.state_dir}") + self.log("if you want to keep the machine state, pass --keep-machine-state") + + def copy_from_machine(self, source: str, target_dir: str = "") -> None: + """Copy a file from the machine (specified by an in-machine source path) to a path + relative to `$out`. The file is copied via the `shared_dir` shared among + all the machines (using a temporary directory). + """ + # Compute the source, target, and intermediate shared file names + vm_src = Path(source) + with tempfile.TemporaryDirectory(dir=self.shared_dir) as shared_td: + shared_temp = Path(shared_td) + vm_shared_temp = Path("/tmp/shared") / shared_temp.name + vm_intermediate = vm_shared_temp / vm_src.name + intermediate = shared_temp / vm_src.name + # Copy the file to the shared directory inside machines + self.succeed(make_command(["mkdir", "-p", vm_shared_temp])) + self.succeed(make_command(["cp", "-r", vm_src, vm_intermediate])) + abs_target = self.out_dir / target_dir / vm_src.name + abs_target.parent.mkdir(exist_ok=True, parents=True) + # Copy the file from the shared directory outside machines + if intermediate.is_dir(): + shutil.copytree(intermediate, abs_target) + else: + shutil.copy(intermediate, abs_target) + + @warnings.deprecated("Use copy_from_machine() instead") + def copy_from_vm(self, source: str, target_dir: str = "") -> None: + self.copy_from_machine(source, target_dir) + + def copy_from_host_via_shell(self, source: str, target: str) -> None: + """Copy a file from the host into the guest by piping it over the + shell into the destination file. Works without host-guest shared folder. + Prefer copy_from_host for whenever possible. + """ + with open(source, "rb") as fh: + content_b64 = base64.b64encode(fh.read()).decode() + self.succeed( + f"mkdir -p $(dirname {target})", + f"echo -n {content_b64} | base64 -d > {target}", + ) + + def copy_from_host(self, source: str, target: str) -> None: + """ + Copies a file from host to machine, e.g., + `copy_from_host("myfile", "/etc/my/important/file")`. + + The first argument is the file on the host. Note that the "host" refers + to the environment in which the test driver runs, which is typically the + Nix build sandbox. + + The second argument is the location of the file on the machine that will + be written to. + + The file is copied via the `shared_dir` directory which is shared among + all the machines (using a temporary directory). + The access rights bits will mimic the ones from the host file and + user:group will be root:root. + """ + host_src = Path(source) + vm_target = Path(target) + with tempfile.TemporaryDirectory(dir=self.shared_dir) as shared_td: + shared_temp = Path(shared_td) + host_intermediate = shared_temp / host_src.name + vm_shared_temp = Path("/tmp/shared") / shared_temp.name + vm_intermediate = vm_shared_temp / host_src.name + + self.succeed(make_command(["mkdir", "-p", vm_shared_temp])) + if host_src.is_dir(): + shutil.copytree(host_src, host_intermediate) + else: + shutil.copy(host_src, host_intermediate) + self.succeed(make_command(["mkdir", "-p", vm_target.parent])) + self.succeed(make_command(["cp", "-r", vm_intermediate, vm_target])) + + +class QemuMachine(BaseMachine): + """A handle to the machine with this name, that also knows how to manage + the machine lifecycle with the help of a start script / command.""" + + name: str + out_dir: Path + shared_dir: Path + state_dir: Path + monitor_path: Path + qmp_path: Path + shell_path: Path + + start_command: QemuStartCommand + + process: subprocess.Popen | None + pid: int | None + monitor: socket.socket | None + qmp_client: QMPSession | None + shell: socket.socket | None + serial_thread: threading.Thread | None + + booted: bool + connected: bool + # Store last serial console lines for use + # of wait_for_console_text + last_lines: Queue = Queue() + # Store all console output for full log retrieval + full_console_log: list[str] + + def __init__( + self, + out_dir: Path, + tmp_dir: Path, + start_command: str, + logger: AbstractLogger, + name: str | None = None, + keep_machine_state: bool = False, + callbacks: list[Callable] | None = None, + ) -> None: + self.start_command = QemuStartCommand(start_command) + super().__init__( + out_dir=out_dir, + name=name or self.start_command.machine_name, + logger=logger, + callbacks=callbacks, + tmp_dir=tmp_dir, + keep_machine_state=keep_machine_state, + ) + + self.full_console_log = [] + + # set up directories + self.monitor_path = self.state_dir / "monitor" + self.qmp_path = self.state_dir / "qmp" + self.shell_path = self.state_dir / "shell" + + self.process = None + self.pid = None + self.monitor = None + self.qmp_client = None + self.shell = None + self.serial_thread = None + + self.booted = False + self.connected = False + + def ssh_backdoor_command(self, index: int) -> str: + return f"ssh -o User=root vsock/{index}" + + def is_up(self) -> bool: + return self.booted and self.connected + + def wait_for_monitor_prompt(self) -> str: + assert self.monitor is not None + answer = "" + while True: + undecoded_answer = self.monitor.recv(1024) + if not undecoded_answer: + break + answer += undecoded_answer.decode() + if answer.endswith("(qemu) "): + break + return answer + + def send_monitor_command(self, command: str) -> str: + """ + Send a command to the QEMU monitor. This allows attaching + virtual USB disks to a running machine, among other things. + """ + self.run_callbacks() + message = f"{command}\n".encode() + assert self.monitor is not None + self.monitor.send(message) + return self.wait_for_monitor_prompt() + + def _next_newline_closed_block_from_shell(self) -> str: + assert self.shell + output_buffer = [] + while True: + # This receives up to 4096 bytes from the socket + chunk = self.shell.recv(4096) + if not chunk: + # Probably a broken pipe, return the output we have + break + + decoded = chunk.decode() + output_buffer += [decoded] + if decoded[-1] == "\n": + break + return "".join(output_buffer) + + def get_tty_text(self, tty: str) -> str: + """ + Get the output printed to a given TTY. + """ + status, output = self.execute( + f"fold -w$(stty -F /dev/tty{tty} size | awk '{{print $2}}') /dev/vcs{tty}" + ) + return output + + def wait_until_tty_matches(self, tty: str, regexp: str, timeout: int = 900) -> None: + """Wait until the visible output on the chosen TTY matches regular + expression. Throws an exception on timeout. + """ + matcher = re.compile(regexp) + + def tty_matches(last_try: bool) -> bool: + text = self.get_tty_text(tty) + if last_try: + self.log( + f"Last chance to match /{regexp}/ on TTY{tty}, " + f"which currently contains: {text}" + ) + return len(matcher.findall(text)) > 0 + + with self.nested(f"waiting for {regexp} to appear on tty {tty}"): + retry(tty_matches, timeout) + + def dump_tty_contents(self, tty: str) -> None: + """Debugging: Dump the contents of the TTY""" + self.execute(f"fold -w 80 /dev/vcs{tty} | systemd-cat") + + def _execute( + self, + command: str, + check_return: bool = True, + check_output: bool = True, + timeout: int | None = 900, + ) -> tuple[int, str]: self.connect() # Always run command with shell opts @@ -598,75 +932,6 @@ class Machine: break self.send_console(char.decode()) - def succeed(self, *commands: str, timeout: int | None = None) -> str: - """ - Execute a shell command, raising an exception if the exit status is - not zero, otherwise returning the standard output. Similar to `execute`, - except that the timeout is `None` by default. See `execute` for details on - command execution. - """ - output = "" - for command in commands: - with self.nested(f"must succeed: {command}"): - (status, out) = self.execute(command, timeout=timeout) - if status != 0: - self.log(f"output: {out}") - raise RequestedAssertionFailed( - f"command `{command}` failed (exit code {status})" - ) - output += out - return output - - def fail(self, *commands: str, timeout: int | None = None) -> str: - """ - Like `succeed`, but raising an exception if the command returns a zero - status. - """ - output = "" - for command in commands: - with self.nested(f"must fail: {command}"): - (status, out) = self.execute(command, timeout=timeout) - if status == 0: - raise RequestedAssertionFailed( - f"command `{command}` unexpectedly succeeded" - ) - output += out - return output - - def wait_until_succeeds(self, command: str, timeout: int = 900) -> str: - """ - Repeat a shell command with 1-second intervals until it succeeds. - Has a default timeout of 900 seconds which can be modified, e.g. - `wait_until_succeeds(cmd, timeout=10)`. See `execute` for details on - command execution. - Throws an exception on timeout. - """ - output = "" - - def check_success(_last_try: bool) -> bool: - nonlocal output - status, output = self.execute(command, timeout=timeout) - return status == 0 - - with self.nested(f"waiting for success: {command}"): - retry(check_success, timeout) - return output - - def wait_until_fails(self, command: str, timeout: int = 900) -> str: - """ - Like `wait_until_succeeds`, but repeating the command until it fails. - """ - output = "" - - def check_failure(_last_try: bool) -> bool: - nonlocal output - status, output = self.execute(command, timeout=timeout) - return status != 0 - - with self.nested(f"waiting for failure: {command}"): - retry(check_failure, timeout) - return output - def wait_for_shutdown(self) -> None: """ Wait for the VM to power off. This does *not* initiate a shutdown; @@ -710,33 +975,6 @@ class Machine: if elapsed >= timeout: raise TimeoutError - def get_tty_text(self, tty: str) -> str: - """ - Get the output printed to a given TTY. - """ - status, output = self.execute( - f"fold -w$(stty -F /dev/tty{tty} size | awk '{{print $2}}') /dev/vcs{tty}" - ) - return output - - def wait_until_tty_matches(self, tty: str, regexp: str, timeout: int = 900) -> None: - """Wait until the visible output on the chosen TTY matches regular - expression. Throws an exception on timeout. - """ - matcher = re.compile(regexp) - - def tty_matches(last_try: bool) -> bool: - text = self.get_tty_text(tty) - if last_try: - self.log( - f"Last chance to match /{regexp}/ on TTY{tty}, " - f"which currently contains: {text}" - ) - return len(matcher.findall(text)) > 0 - - with self.nested(f"waiting for {regexp} to appear on tty {tty}"): - retry(tty_matches, timeout) - def send_chars(self, chars: str, delay: float | None = 0.01) -> None: r""" Simulate typing a sequence of characters on the virtual keyboard, @@ -759,70 +997,6 @@ class Machine: with self.nested(f"waiting for file '{filename}'"): retry(check_file, timeout) - def wait_for_open_port( - self, port: int, addr: str = "localhost", timeout: int = 900 - ) -> None: - """ - Wait until a process is listening on the given TCP port and IP address - (default `localhost`). - """ - - def port_is_open(_last_try: bool) -> bool: - status, _ = self.execute(f"nc -z {addr} {port}") - return status == 0 - - with self.nested(f"waiting for TCP port {port} on {addr}"): - retry(port_is_open, timeout) - - def wait_for_open_unix_socket( - self, addr: str, is_datagram: bool = False, timeout: int = 900 - ) -> None: - """ - Wait until a process is listening on the given UNIX-domain socket - (default to a UNIX-domain stream socket). - """ - - nc_flags = [ - "-z", - "-uU" if is_datagram else "-U", - ] - - def socket_is_open(_last_try: bool) -> bool: - status, _ = self.execute(f"nc {' '.join(nc_flags)} {addr}") - return status == 0 - - with self.nested( - f"waiting for UNIX-domain {'datagram' if is_datagram else 'stream'} on '{addr}'" - ): - retry(socket_is_open, timeout) - - def wait_for_closed_port( - self, port: int, addr: str = "localhost", timeout: int = 900 - ) -> None: - """ - Wait until nobody is listening on the given TCP port and IP address - (default `localhost`). - """ - - def port_is_closed(_last_try: bool) -> bool: - status, _ = self.execute(f"nc -z {addr} {port}") - return status != 0 - - with self.nested(f"waiting for TCP port {port} on {addr} to be closed"): - retry(port_is_closed, timeout) - - def start_job(self, jobname: str, user: str | None = None) -> tuple[int, str]: - """ - Start systemd service. - """ - return self.systemctl(f"start {jobname}", user) - - def stop_job(self, jobname: str, user: str | None = None) -> tuple[int, str]: - """ - Stop systemd service. - """ - return self.systemctl(f"stop {jobname}", user) - def connect(self) -> None: """ Wait for a connection to the guest root shell @@ -902,78 +1076,6 @@ class Machine: f"Cannot convert screenshot (pnmtopng returned code {ret.returncode})" ) - def copy_from_host_via_shell(self, source: str, target: str) -> None: - """Copy a file from the host into the guest by piping it over the - shell into the destination file. Works without host-guest shared folder. - Prefer copy_from_host for whenever possible. - """ - with open(source, "rb") as fh: - content_b64 = base64.b64encode(fh.read()).decode() - self.succeed( - f"mkdir -p $(dirname {target})", - f"echo -n {content_b64} | base64 -d > {target}", - ) - - def copy_from_host(self, source: str, target: str) -> None: - """ - Copies a file from host to machine, e.g., - `copy_from_host("myfile", "/etc/my/important/file")`. - - The first argument is the file on the host. Note that the "host" refers - to the environment in which the test driver runs, which is typically the - Nix build sandbox. - - The second argument is the location of the file on the machine that will - be written to. - - The file is copied via the `shared_dir` directory which is shared among - all the VMs (using a temporary directory). - The access rights bits will mimic the ones from the host file and - user:group will be root:root. - """ - host_src = Path(source) - vm_target = Path(target) - with tempfile.TemporaryDirectory(dir=self.shared_dir) as shared_td: - shared_temp = Path(shared_td) - host_intermediate = shared_temp / host_src.name - vm_shared_temp = Path("/tmp/shared") / shared_temp.name - vm_intermediate = vm_shared_temp / host_src.name - - self.succeed(make_command(["mkdir", "-p", vm_shared_temp])) - if host_src.is_dir(): - shutil.copytree(host_src, host_intermediate) - else: - shutil.copy(host_src, host_intermediate) - self.succeed(make_command(["mkdir", "-p", vm_target.parent])) - self.succeed(make_command(["cp", "-r", vm_intermediate, vm_target])) - - def copy_from_vm(self, source: str, target_dir: str = "") -> None: - """Copy a file from the VM (specified by an in-VM source path) to a path - relative to `$out`. The file is copied via the `shared_dir` shared among - all the VMs (using a temporary directory). - """ - # Compute the source, target, and intermediate shared file names - vm_src = Path(source) - with tempfile.TemporaryDirectory(dir=self.shared_dir) as shared_td: - shared_temp = Path(shared_td) - vm_shared_temp = Path("/tmp/shared") / shared_temp.name - vm_intermediate = vm_shared_temp / vm_src.name - intermediate = shared_temp / vm_src.name - # Copy the file to the shared directory inside VM - self.succeed(make_command(["mkdir", "-p", vm_shared_temp])) - self.succeed(make_command(["cp", "-r", vm_src, vm_intermediate])) - abs_target = self.out_dir / target_dir / vm_src.name - abs_target.parent.mkdir(exist_ok=True, parents=True) - # Copy the file from the shared directory outside VM - if intermediate.is_dir(): - shutil.copytree(intermediate, abs_target) - else: - shutil.copy(intermediate, abs_target) - - def dump_tty_contents(self, tty: str) -> None: - """Debugging: Dump the contents of the TTY""" - self.execute(f"fold -w 80 /dev/vcs{tty} | systemd-cat") - def get_screen_text_variants(self) -> list[str]: """ Return a list of different interpretations of what is currently @@ -1154,11 +1256,6 @@ class Machine: self.log(f"QEMU running (pid {self.pid})") - def cleanup_statedir(self) -> None: - shutil.rmtree(self.state_dir) - self.logger.log(f"deleting VM state directory {self.state_dir}") - self.logger.log("if you want to keep the VM state, pass --keep-vm-state") - def shutdown(self) -> None: """ Shut down the machine, waiting for the VM to exit. @@ -1234,10 +1331,6 @@ class Machine: with self.nested("waiting for a window to appear"): retry(window_is_visible, timeout) - def sleep(self, secs: int) -> None: - # We want to sleep in *guest* time, not *host* time. - self.succeed(f"sleep {secs}") - def forward_port(self, host_port: int = 8080, guest_port: int = 80) -> None: """ Forward a TCP port on the host to a TCP port on the guest. @@ -1264,7 +1357,7 @@ class Machine: def release(self) -> None: if self.pid is None: return - self.logger.info(f"kill machine (pid {self.pid})") + self.logger.info(f"kill QemuMachine (pid {self.pid})") assert self.process assert self.shell assert self.monitor @@ -1278,10 +1371,6 @@ class Machine: if self.qmp_client: self.qmp_client.close() - def run_callbacks(self) -> None: - for callback in self.callbacks: - callback() - def switch_root(self) -> None: """ Transition from stage 1 to stage 2. This requires the @@ -1296,3 +1385,283 @@ class Machine: ) self.connected = False self.connect() + + +class NspawnMachine(BaseMachine): + """ + A handle to a systemd-nspawn container machine with this name, that also + knows how to manage the machine lifecycle with the help of a start script / command. + """ + + start_command: str + tmp_dir: Path + process: subprocess.Popen | None + pid: int | None + + machine_sock_path: Path + machine_sock: socket.socket | None + + @staticmethod + def machine_name_from_start_command(start_command: str) -> str: + match = re.search("run-(.+)-nspawn", os.path.basename(start_command)) + assert match is not None, f"Could not extract node name from {start_command}" + return match.group(1) + + def __init__( + self, + out_dir: Path, + name: str | None, + start_command: str, + tmp_dir: Path, + logger: AbstractLogger, + callbacks: list[Callable] | None = None, + keep_machine_state: bool = False, + ): + # TODO: don't compute `name` from `start_command` path, instead thread it down explicitly. + # See analogous TODO in `QemuStartCommand::machine_name`. + super().__init__( + out_dir=out_dir, + name=name or self.machine_name_from_start_command(start_command), + logger=logger, + callbacks=callbacks, + tmp_dir=tmp_dir, + keep_machine_state=keep_machine_state, + ) + + self.start_command = start_command + self.process = None + self.pid = None + + self.machine_sock_path = self.tmp_dir / f"{self.name}-nspawn.sock" + + def ssh_backdoor_command(self, index: int) -> str: + # documented in systemd-ssh-generator(8) and https://systemd.io/CONTAINER_INTERFACE/ + socket_path = f"/run/systemd/nspawn/unix-export/{self.name}/ssh" + proxy_cmd = f"socat - UNIX-CLIENT:{socket_path}" + return f'ssh -o User=root -o ProxyCommand="{proxy_cmd}" bash' + + def release(self) -> None: + if self.pid is None: + return + + if self.machine_sock: + self.machine_sock.close() + + self.logger.info(f"kill NspawnMachine (pid {self.pid})") + assert self.process is not None + self.process.terminate() + self.process = None + + def is_up(self) -> bool: + return self.process is not None + + def _poll_socket(self) -> tuple[bool, int | None]: + """Non-blocking check of container status via socket. + Returns (is_ready, leader_pid). + """ + assert self.machine_sock is not None + ready = False + leader_pid = None + try: + data, _ = self.machine_sock.recvfrom(4096) + msg = data.decode() + for line in msg.splitlines(): + if line == "READY=1": + ready = True + if line.startswith("X_NSPAWN_LEADER_PID="): + leader_pid = int(line.split("=")[1]) + except OSError: + pass + return ready, leader_pid + + @cached_property + def get_systemd_process(self) -> int: + """Block until startup is complete and return the PID of the container's systemd process.""" + assert self.process is not None + + container_pid: int | None = None + is_ready = False + + start_time = time.monotonic() + last_warning = start_time + delay = 0.01 + max_delay = 0.5 + + while not is_ready or container_pid is None: + # Poll the socket until we have the container leader PID + if self.process.poll() is not None: + raise MachineError("systemd-nspawn process exited unexpectedly") + + # Print periodic warnings every 10s so the user knows we aren't deadlocked + now = time.monotonic() + if now - last_warning > 10.0: + self.log( + f"still waiting for container '{self.name}' to reach ready state..." + ) + last_warning = now + + # Poll and update our local tracking variables + ready_now, pid_now = self._poll_socket() + if ready_now: + is_ready = True + if pid_now: + container_pid = pid_now + + if not (is_ready and container_pid): + time.sleep(delay) + delay = min(delay * 2, max_delay) + + return container_pid + + def _execute( + self, + command: str, + check_return: bool = True, + check_output: bool = True, + timeout: int | None = 900, + ) -> tuple[int, str]: + self.start() + + container_pid = self.get_systemd_process + nsenter = shutil.which("nsenter") + assert nsenter is not None + + # Sourcing /etc/profile on every call of `_execute` ensures a correct shell + # environment (correct PATH, etc.). This is slower than the QEMU version. + # + # NOTE If the test calls switch-to-configuration (with a differently configured specialization) + # this will use the /etc/profile of the new specialisation while `QemuMachine` nodes + # will continue to use the original /etc/profile. + command = f"set -eo pipefail; source /etc/profile; set -u; {command}" + + cp = subprocess.run( + [ + nsenter, + "--target", + str(container_pid), + "--mount", + "--uts", + "--ipc", + "--net", + "--pid", + "--cgroup", + "/bin/sh", + "-c", + command, + ], + env={}, + timeout=timeout, + stdout=subprocess.PIPE, + text=True, + ) + return (cp.returncode, cp.stdout) + + def _stream_journal(self) -> None: + assert self.process is not None, "Container not started" + journal_path = self.state_dir / "var/log/journal" + + # Grab a reference to the process here so we can continue polling + # the container process to see if it has exited. + proc = self.process + + # 1. Wait for the directory to actually be created by the container + self.log(f"Waiting for journal at {journal_path}...") + max_attempts = 10 + attempts = 0 + while not journal_path.exists() and attempts < max_attempts: + time.sleep(1) + attempts += 1 + + if not journal_path.exists(): + self.log(f"Error: Journal directory {journal_path} never appeared.") + return + + # 2. Start the journalctl process + # Using a loop here handles cases where journalctl might exit unexpectedly + while proc.poll() is None: # While the container is still running + with subprocess.Popen( + [ + "journalctl", + "--follow", + f"--directory={journal_path}", + "--lines=all", + "--output=short-monotonic", + ], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1, # Line buffered. + ) as log_proc: + assert log_proc.stdout is not None, ( + "Failed to capture journalctl output" + ) + try: + for line in iter(log_proc.stdout.readline, ""): + if line: + self.log_serial(line.rstrip()) + if proc.poll() is not None: + break + except Exception as e: + self.log(f"Error while reading journalctl output: {e}") + finally: + log_proc.terminate() + log_proc.wait() + + # If we reach here, journalctl stopped while the container is still running. + # Wait a moment before retrying to avoid CPU pegging if something is wrong. + if proc.poll() is None: + time.sleep(1) + + def start(self) -> None: + """ + Start the systemd-nspawn container. This method is asynchronous --- it does + not wait for the container to finish booting. + """ + if self.process is not None: + return + + if self.machine_sock_path is not None and self.machine_sock_path.exists(): + self.machine_sock_path.unlink() + + self.machine_sock = socket.socket(family=socket.AF_UNIX, type=socket.SOCK_DGRAM) + self.machine_sock.bind(str(self.machine_sock_path)) + self.machine_sock.setblocking(False) + + self.process = subprocess.Popen( + [self.start_command], + env={ + "RUN_NSPAWN_ROOT_DIR": str(self.state_dir), + "RUN_NSPAWN_SHARED_DIR": str(self.shared_dir), + "NOTIFY_SOCKET": self.machine_sock_path.as_posix(), + }, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + + self.pid = self.process.pid + + self.log(f"systemd-nspawn running (pid {self.pid})") + + journal_thread = threading.Thread(target=self._stream_journal, daemon=True) + journal_thread.start() + + def shutdown(self) -> None: + """ + Shut down the container, waiting for it to exit. + """ + if self.process is None: + return + self.systemctl("poweroff") + self.wait_for_shutdown() + + def wait_for_shutdown(self) -> None: + """ + Wait for the container to power off. This does *not* initiate a shutdown; + that's usually done via `shutdown()`. + """ + if self.process is None: + return + + with self.nested("waiting for the container to power off"): + self.process.wait() + self.process = None diff --git a/nixos/lib/test-driver/src/test_driver/vlan.py b/nixos/lib/test-driver/src/test_driver/vlan.py index 89ca33165b4d..eed66dcd13cb 100644 --- a/nixos/lib/test-driver/src/test_driver/vlan.py +++ b/nixos/lib/test-driver/src/test_driver/vlan.py @@ -50,6 +50,8 @@ class VLan: pid: int fd: io.TextIOBase + plug_process: subprocess.Popen + logger: AbstractLogger def __repr__(self) -> str: @@ -58,6 +60,7 @@ class VLan: def __init__(self, nr: int, tmp_dir: Path, logger: AbstractLogger): self.nr = nr self.socket_dir = tmp_dir / f"vde{self.nr}.ctl" + self.tap_name = f"vde-tap{self.nr}" self.logger = logger # TODO: don't side-effect environment here @@ -114,6 +117,13 @@ class VLan: if "1000 Success" in line: break + # This is needed to allow systemd-nspawn containers to communicate + # with VMs connected to the VLAN. + self.logger.info(f"creating tap interface {self.tap_name}") + self.plug_process = subprocess.Popen( + ["vde_plug2tap", "-s", self.socket_dir, self.tap_name], + ) + assert (self.socket_dir / "ctl").exists(), "cannot start vde_switch" self.logger.info(f"running vlan (pid {self.pid}; ctl {self.socket_dir})") @@ -122,4 +132,7 @@ class VLan: self.logger.info(f"kill vlan (pid {self.pid})") assert self.process.stdin is not None self.process.stdin.close() + if self.plug_process: + self.plug_process.terminate() + self.plug_process.wait() self.process.terminate() diff --git a/nixos/lib/test-script-prepend.py b/nixos/lib/test-script-prepend.py index 6be20270c6cc..b9a12d5f2f54 100644 --- a/nixos/lib/test-script-prepend.py +++ b/nixos/lib/test-script-prepend.py @@ -4,7 +4,7 @@ from test_driver.debug import DebugAbstract from test_driver.driver import Driver from test_driver.vlan import VLan -from test_driver.machine import Machine +from test_driver.machine import BaseMachine, NspawnMachine, QemuMachine from test_driver.logger import AbstractLogger from typing import Callable, Iterator, ContextManager, Optional, List, Dict, Any, Union from typing_extensions import Protocol @@ -34,8 +34,9 @@ class CreateMachineProtocol(Protocol): start_command: str | dict, *, name: Optional[str] = None, - keep_vm_state: bool = False, - ) -> Machine: + keep_machine_state: bool = False, + **kwargs: Any, # to allow usage of deprecated keep_vm_state + ) -> BaseMachine: raise Exception("This is just type information for the Nix test driver") @@ -43,7 +44,7 @@ start_all: Callable[[], None] subtest: Callable[[str], ContextManager[None]] retry: RetryProtocol test_script: Callable[[], None] -machines: List[Machine] +machines: List[BaseMachine] vlans: List[VLan] driver: Driver log: AbstractLogger diff --git a/nixos/lib/testing-python.nix b/nixos/lib/testing-python.nix index 878f9669321a..1d2777ecb033 100644 --- a/nixos/lib/testing-python.nix +++ b/nixos/lib/testing-python.nix @@ -56,6 +56,7 @@ pkgs.lib.throwIf (args ? specialArgs) { machine ? null, nodes ? { }, + containers ? { }, testScript, enableOCR ? false, globalTimeout ? (60 * 60), diff --git a/nixos/lib/testing/driver.nix b/nixos/lib/testing/driver.nix index 5845ebe2695a..63ad51147743 100644 --- a/nixos/lib/testing/driver.nix +++ b/nixos/lib/testing/driver.nix @@ -14,18 +14,17 @@ let qemu_pkg = config.qemu.package; imagemagick_light = hostPkgs.imagemagick_light.override { inherit (hostPkgs) libtiff; }; tesseract4 = hostPkgs.tesseract4.override { enableLanguages = [ "eng" ]; }; + + enableNspawn = config.containers != { }; + # We want `pkgs.systemd`, *not* `python3Packages.system`. + systemd = hostPkgs.systemd; }; vlans = map ( m: (m.virtualisation.vlans ++ (lib.mapAttrsToList (_: v: v.vlan) m.virtualisation.interfaces)) - ) (lib.attrValues config.nodes); + ) ((lib.attrValues config.nodes) ++ (lib.attrValues config.containers)); vms = map (m: m.system.build.vm) (lib.attrValues config.nodes); - - nodeHostNames = - let - nodesList = map (c: c.system.name) (lib.attrValues config.nodes); - in - nodesList ++ lib.optional (lib.length nodesList == 1 && !lib.elem "machine" nodesList) "machine"; + containers = map (m: m.system.build.nspawn) (lib.attrValues config.containers); pythonizeName = name: @@ -38,8 +37,22 @@ let uniqueVlans = lib.unique (builtins.concatLists vlans); vlanNames = map (i: "vlan${toString i}: VLan;") uniqueVlans; - pythonizedNames = map pythonizeName nodeHostNames; - machineNames = map (name: "${name}: Machine;") pythonizedNames; + + vmMachineNames = map (c: c.system.name) (lib.attrValues config.nodes); + containerMachineNames = map (c: c.system.name) (lib.attrValues config.containers); + + theOnlyMachine = + let + exactlyOneMachine = lib.length (lib.attrValues config.nodes) == 1; + allMachineNames = map (c: c.system.name) (lib.attrValues config.allMachines); + in + lib.optional (exactlyOneMachine && !lib.elem "machine" allMachineNames) "machine"; + + pythonizedVmNames = map pythonizeName (vmMachineNames ++ theOnlyMachine); + vmMachineTypeHints = map (name: "${name}: QemuMachine;") pythonizedVmNames; + + pythonizedContainerNames = map pythonizeName containerMachineNames; + containerMachineTypeHints = map (name: "${name}: NspawnMachine;") pythonizedContainerNames; withChecks = lib.warnIf config.skipLint "Linting is disabled"; @@ -62,12 +75,16 @@ let '' mkdir -p $out/bin - vmStartScripts=($(for i in ${toString vms}; do echo $i/bin/run-*-vm; done)) + vmNames=(${lib.escapeShellArgs vmMachineNames}) + vmStartScripts=(${lib.escapeShellArgs (map lib.getExe vms)}) + containerNames=(${lib.escapeShellArgs containerMachineNames}) + containerStartScripts=(${lib.escapeShellArgs (map lib.getExe containers)}) ${lib.optionalString (!config.skipTypeCheck) '' # prepend type hints so the test script can be type checked with mypy cat "${../test-script-prepend.py}" >> testScriptWithTypes - echo "${toString machineNames}" >> testScriptWithTypes + echo "${toString vmMachineTypeHints}" >> testScriptWithTypes + echo "${toString containerMachineTypeHints}" >> testScriptWithTypes echo "${toString vlanNames}" >> testScriptWithTypes echo -n "$testScript" >> testScriptWithTypes @@ -90,7 +107,9 @@ let echo "See https://nixos.org/manual/nixos/stable/#test-opt-skipLint" PYFLAKES_BUILTINS="$( - echo -n ${lib.escapeShellArg (lib.concatStringsSep "," pythonizedNames)}, + echo -n ${ + lib.escapeShellArg (lib.concatStringsSep "," (pythonizedVmNames ++ pythonizedContainerNames)) + }, cat ${lib.escapeShellArg "driver-symbols"} )" ${hostPkgs.python3Packages.pyflakes}/bin/pyflakes $out/test-script ''} @@ -98,7 +117,10 @@ let # set defaults through environment # see: ./test-driver/test-driver.py argparse implementation wrapProgram $out/bin/nixos-test-driver \ - --set startScripts "''${vmStartScripts[*]}" \ + --set vmStartScripts "''${vmStartScripts[*]}" \ + --set vmNames "''${vmNames[*]}" \ + --set containerStartScripts "''${containerStartScripts[*]}" \ + --set containerNames "''${containerNames[*]}" \ --set testScript "$out/test-script" \ --set globalTimeout "${toString config.globalTimeout}" \ --set vlans '${toString vlans}' \ diff --git a/nixos/lib/testing/network.nix b/nixos/lib/testing/network.nix index 9a5facfc2433..570b854d8e28 100644 --- a/nixos/lib/testing/network.nix +++ b/nixos/lib/testing/network.nix @@ -1,11 +1,13 @@ -{ lib, nodes, ... }: +testModuleArgs@{ + lib, + ... +}: let inherit (lib) attrNames - concatMap + concatMapAttrsStringSep concatMapStrings - flip forEach head listToAttrs @@ -20,22 +22,15 @@ let zipLists ; - nodeNumbers = listToAttrs (zipListsWith nameValuePair (attrNames nodes) (range 1 254)); + nodeNumbers = listToAttrs ( + zipListsWith nameValuePair (attrNames testModuleArgs.config.allMachines) (range 1 254) + ); networkModule = - { - config, - nodes, - pkgs, - ... - }: + { config, ... }: let - qemu-common = import ../qemu-common.nix { inherit (pkgs) lib stdenv; }; - interfaces = lib.attrValues config.virtualisation.allInterfaces; - interfacesNumbered = zipLists interfaces (range 1 255); - # Automatically assign IP addresses to requested interfaces. assignIPs = lib.filter (i: i.assignIP) interfaces; ipInterfaces = forEach assignIPs ( @@ -56,17 +51,6 @@ let } ); - qemuOptions = lib.flatten ( - forEach interfacesNumbered ( - { fst, snd }: qemu-common.qemuNICFlags snd fst.vlan config.virtualisation.test.nodeNumber - ) - ); - udevRules = forEach interfaces ( - interface: - # MAC Addresses for QEMU network devices are lowercase, and udev string comparison is case-sensitive. - ''SUBSYSTEM=="net",ACTION=="add",ATTR{address}=="${toLower (qemu-common.qemuNicMac interface.vlan config.virtualisation.test.nodeNumber)}",NAME="${interface.name}"'' - ); - networkConfig = { networking.hostName = mkDefault config.virtualisation.test.nodeName; @@ -80,33 +64,51 @@ let optionalString (ipInterfaces != [ ]) (head (head ipInterfaces).value.ipv6.addresses).address; - # Put the IP addresses of all VMs in this machine's - # /etc/hosts file. If a machine has multiple - # interfaces, use the IP address corresponding to - # the first interface (i.e. the first network in its - # virtualisation.vlans option). - networking.extraHosts = flip concatMapStrings (attrNames nodes) ( - m': + # Generate /etc/hosts including every remote's primary IP addresses + # (whichever VLAN they may belong to) as well as all IP addresses from + # VLANs that both the local machine and the remote machine share. + networking.extraHosts = let - config = nodes.${m'}; - hostnames = - optionalString ( - config.networking.domain != null - ) "${config.networking.hostName}.${config.networking.domain} " - + "${config.networking.hostName}\n"; + localVlans = config.virtualisation.vlans; in - optionalString ( - config.networking.primaryIPAddress != "" - ) "${config.networking.primaryIPAddress} ${hostnames}" - + optionalString ( - config.networking.primaryIPv6Address != "" - ) "${config.networking.primaryIPv6Address} ${hostnames}" - ); + concatMapAttrsStringSep "" ( + mName: remoteConfig: + let + remoteInterfaces = remoteConfig.networking.interfaces; + sharedIps = lib.flatten ( + lib.mapAttrsToList ( + ifaceName: ifaceCfg: + let + remoteIfaceMeta = remoteConfig.virtualisation.allInterfaces."${ifaceName}" or { }; + vlanId = remoteIfaceMeta.vlan or null; + in + if vlanId != null && builtins.elem vlanId localVlans then + builtins.map (addr: addr.address) ifaceCfg.ipv4.addresses + ++ builtins.map (addr: addr.address) ifaceCfg.ipv6.addresses + else + [ ] + ) remoteInterfaces + ); - virtualisation.qemu.options = qemuOptions; - boot.initrd.services.udev.rules = concatMapStrings (x: x + "\n") udevRules; + # We also want to test router protocols that enable connections + # between nodes even if they don't share a VLAN, so we include + # the primary IPs of all machines in the hosts file. + primaryIPs = [ + remoteConfig.networking.primaryIPAddress + remoteConfig.networking.primaryIPv6Address + ]; + + allReachableIps = lib.lists.uniqueStrings (sharedIps ++ primaryIPs); + + hostnames = + optionalString ( + remoteConfig.networking.domain != null + ) "${remoteConfig.networking.hostName}.${remoteConfig.networking.domain} " + + "${remoteConfig.networking.hostName}\n"; + in + builtins.concatStringsSep "" (map (ip: "${ip} ${hostnames}") allReachableIps) + ) testModuleArgs.config.allMachines; }; - in { key = "network-interfaces"; @@ -117,6 +119,31 @@ let }; }; + qemuNetworkModule = + { config, pkgs, ... }: + let + qemu-common = import ../qemu-common.nix { inherit (pkgs) lib stdenv; }; + + interfaces = lib.attrValues config.virtualisation.allInterfaces; + + interfacesNumbered = zipLists interfaces (range 1 255); + + qemuOptions = lib.flatten ( + forEach interfacesNumbered ( + { fst, snd }: qemu-common.qemuNICFlags snd fst.vlan config.virtualisation.test.nodeNumber + ) + ); + udevRules = map ( + interface: + # MAC Addresses for QEMU network devices are lowercase, and udev string comparison is case-sensitive. + ''SUBSYSTEM=="net",ACTION=="add",ATTR{address}=="${toLower (qemu-common.qemuNicMac interface.vlan config.virtualisation.test.nodeNumber)}",NAME="${interface.name}"'' + ) interfaces; + in + { + virtualisation.qemu.options = qemuOptions; + boot.initrd.services.udev.rules = concatMapStrings (x: x + "\n") udevRules; + }; + nodeNumberModule = ( regular@{ config, name, ... }: { @@ -127,7 +154,7 @@ let # We need to force this in specialisations, otherwise it'd be # readOnly = true; description = '' - The `name` in `nodes.`; stable across `specialisations`. + The `name` in `nodes.` and `containers.`; stable across `specialisations`. ''; }; virtualisation.test.nodeNumber = mkOption { @@ -136,7 +163,7 @@ let readOnly = true; default = nodeNumbers.${config.virtualisation.test.nodeName}; description = '' - A unique number assigned for each node in `nodes`. + A unique number assigned for each machine in `nodes` and `containers`. ''; }; @@ -172,5 +199,10 @@ in nodeNumberModule ]; }; + extraBaseNodeModules = { + imports = [ + qemuNetworkModule + ]; + }; }; } diff --git a/nixos/lib/testing/nixos-test-base.nix b/nixos/lib/testing/nixos-test-base.nix index 23358f2185b1..6b518e39ac11 100644 --- a/nixos/lib/testing/nixos-test-base.nix +++ b/nixos/lib/testing/nixos-test-base.nix @@ -7,7 +7,6 @@ let in { imports = [ - ../../modules/virtualisation/qemu-vm.nix ../../modules/testing/test-instrumentation.nix # !!! should only get added for automated test runs { key = "no-manual"; @@ -32,7 +31,9 @@ in # This is mostly a Hydra optimization, so we don't rebuild all the tests every time switch-to-configuration-ng changes. key = "no-switch-to-configuration"; system.switch.enable = mkDefault ( - config.isSpecialisation || config.specialisation != { } || config.virtualisation.installBootLoader + config.isSpecialisation + || config.specialisation != { } + || (!config.boot.isContainer && config.virtualisation.installBootLoader) ); } ) diff --git a/nixos/lib/testing/nodes.nix b/nixos/lib/testing/nodes.nix index 721a3c88b369..7384dec68895 100644 --- a/nixos/lib/testing/nodes.nix +++ b/nixos/lib/testing/nodes.nix @@ -2,7 +2,6 @@ testModuleArgs@{ config, lib, hostPkgs, - nodes, options, ... }: @@ -12,12 +11,9 @@ let literalExpression literalMD mapAttrs - mkDefault mkIf mkMerge mkOption - mkForce - optional optionalAttrs types ; @@ -49,15 +45,11 @@ let ./nixos-test-base.nix { key = "nodes"; - _module.args.nodes = config.nodesCompat; + _module.args = { + inherit (config) containers; + nodes = config.nodesCompat; + }; } - ( - { config, ... }: - { - virtualisation.qemu.package = testModuleArgs.config.qemu.package; - virtualisation.host.pkgs = hostPkgs; - } - ) ( { options, ... }: { @@ -73,6 +65,62 @@ let testModuleArgs.config.extraBaseModules ]; }; + baseQemuOS = baseOS.extendModules { + modules = [ + ../../modules/virtualisation/qemu-vm.nix + config.nodeDefaults + { + key = "base-qemu"; + virtualisation.qemu.package = testModuleArgs.config.qemu.package; + virtualisation.host.pkgs = hostPkgs; + } + testModuleArgs.config.extraBaseNodeModules + ]; + }; + baseNspawnOS = baseOS.extendModules { + modules = [ + ../../modules/virtualisation/nspawn-container + config.containerDefaults + ( + { pkgs, ... }: + { + key = "base-nspawn"; + + # PAM requires setuid and doesn't work in the build sandbox. + # https://github.com/NixOS/nix/blob/959c244a1265f4048390f3ad21679219d7b27a99/src/libstore/unix/build/linux-derivation-builder.cc#L63 + services.openssh.settings.UsePAM = false; + + # Networking for tests is statically configured by default. + # dhcpcd times out after blocking for a long time, which slows down tests. + # See https://github.com/NixOS/nixpkgs/pull/478109#discussion_r2867570799 + networking.useDHCP = lib.mkDefault false; + + # Disable Info manual directory generation to prevent build failures. + # + # Context: 'install-info' (from texinfo) is triggered during system-path + # generation to index manuals, but it requires 'gzip' in the $PATH to + # decompress them. + # When 'networking.useDHCP' is set to false, transitive dependencies + # (like dhcpcd or other network tools) that normally pull 'gzip' into + # the system environment are removed. This leaves 'install-info' + # stranded without 'gzip', causing the 'system-path' derivation to fail. + # Since nspawn containers are typically minimal, disabling 'info' + # is a cleaner fix than explicitly adding 'gzip' to systemPackages. + documentation.info.enable = lib.mkDefault false; + + # Gross, insecure hack to make login work. See above. + security.pam.services.login = { + text = '' + auth sufficient ${pkgs.linux-pam}/lib/security/pam_permit.so + account sufficient ${pkgs.linux-pam}/lib/security/pam_permit.so + password sufficient ${pkgs.linux-pam}/lib/security/pam_permit.so + session sufficient ${pkgs.linux-pam}/lib/security/pam_permit.so + ''; + }; + } + ) + ]; + }; # TODO (lib): Dedup with run.nix, add to lib/options.nix mkOneUp = opt: f: lib.mkOverride (opt.highestPrio - 1) (f opt.value); @@ -109,25 +157,74 @@ in node.type = mkOption { type = types.raw; - default = baseOS.type; + default = baseQemuOS.type; internal = true; }; nodes = mkOption { type = types.lazyAttrsOf config.node.type; + default = { }; visible = "shallow"; description = '' - An attribute set of NixOS configuration modules. + An attribute set of NixOS configuration modules representing QEMU vms that can be started during a test. The configurations are augmented by the [`defaults`](#test-opt-defaults) option. They are assigned network addresses according to the `nixos/lib/testing/network.nix` module. - A few special options are available, that aren't in a plain NixOS configuration. See [Configuring the nodes](#sec-nixos-test-nodes) + A few special options are available, that aren't in a plain NixOS configuration. See [Configuring virtual machines](#ssec-nixos-test-qemu-vms) ''; }; + container.type = mkOption { + type = types.raw; + default = baseNspawnOS.type; + internal = true; + }; + + containers = mkOption { + type = types.lazyAttrsOf config.container.type; + default = { }; + visible = "shallow"; + description = '' + An attribute set of NixOS configuration modules representing systemd-nspawn containers that can be started during a test. + + The configurations are augmented by the [`defaults`](#test-opt-defaults) option. + + They are assigned network addresses according to the `nixos/lib/testing/network.nix` module. + + A few special options are available, that aren't in a plain NixOS configuration. See [Configuring containers](#ssec-nixos-test-nspawn-containers) + ''; + }; + + allMachines = mkOption { + readOnly = true; + internal = true; + description = '' + Basically a merge of [{option}`nodes`](#test-opt-nodes) and [{option}`containers`](#test-opt-containers). + + This ensures that there are no name collisions between nodes and containers. + ''; + default = + let + overlappingNames = lib.intersectLists (lib.attrNames config.nodes) ( + lib.attrNames config.containers + ); + in + lib.throwIfNot (overlappingNames == [ ]) + "The following names are used in both `nodes` and `containers`: ${lib.concatStringsSep ", " overlappingNames}" + (config.nodes // config.containers); + }; + defaults = mkOption { + description = '' + NixOS configuration that is applied to all [{option}`nodes`](#test-opt-nodes) and [{option}`containers`](#test-opt-containers). + ''; + type = types.deferredModule; + default = { }; + }; + + nodeDefaults = mkOption { description = '' NixOS configuration that is applied to all [{option}`nodes`](#test-opt-nodes). ''; @@ -135,7 +232,23 @@ in default = { }; }; + containerDefaults = mkOption { + description = '' + NixOS configuration that is applied to all [{option}`containers`](#test-opt-containers). + ''; + type = types.deferredModule; + default = { }; + }; + extraBaseModules = mkOption { + description = '' + NixOS configuration that, like [{option}`defaults`](#test-opt-defaults), is applied to all [{option}`nodes`](#test-opt-nodes) and [{option}`containers`](#test-opt-containers) and can not be undone with [`specialisation..inheritParentConfig`](https://search.nixos.org/options?show=specialisation.%3Cname%3E.inheritParentConfig&from=0&size=50&sort=relevance&type=packages&query=specialisation). + ''; + type = types.deferredModule; + default = { }; + }; + + extraBaseNodeModules = mkOption { description = '' NixOS configuration that, like [{option}`defaults`](#test-opt-defaults), is applied to all [{option}`nodes`](#test-opt-nodes) and can not be undone with [`specialisation..inheritParentConfig`](https://search.nixos.org/options?show=specialisation.%3Cname%3E.inheritParentConfig&from=0&size=50&sort=relevance&type=packages&query=specialisation). ''; @@ -145,7 +258,7 @@ in node.pkgs = mkOption { description = '' - The Nixpkgs to use for the nodes. + The Nixpkgs to use for the nodes and containers. Setting this will make the `nixpkgs.*` options read-only, to avoid mistakenly testing with a Nixpkgs configuration that diverges from regular use. ''; @@ -160,7 +273,7 @@ in description = '' Whether to make the `nixpkgs.*` options read-only. This is only relevant when [`node.pkgs`](#test-opt-node.pkgs) is set. - Set this to `false` when any of the [`nodes`](#test-opt-nodes) needs to configure any of the `nixpkgs.*` options. This will slow down evaluation of your test a bit. + Set this to `false` when any of the [`nodes`](#test-opt-nodes) or [{option}`containers`](#test-opt-containers) need to configure any of the `nixpkgs.*` options. This will slow down evaluation of your test a bit. ''; type = types.bool; default = config.node.pkgs != null; @@ -188,6 +301,7 @@ in }; config = { + _module.args.containers = config.containers; _module.args.nodes = config.nodesCompat; nodesCompat = mapAttrs ( name: config: @@ -201,6 +315,7 @@ in ) config.nodes; passthru.nodes = config.nodesCompat; + passthru.containers = config.containers; extraDriverArgs = mkIf config.sshBackdoor.enable [ "--dump-vsocks=${toString config.sshBackdoor.vsockOffset}" @@ -211,33 +326,35 @@ in nixpkgs.pkgs = config.node.pkgs; imports = [ ../../modules/misc/nixpkgs/read-only.nix ]; }) - (mkIf config.sshBackdoor.enable ( - let - inherit (config.sshBackdoor) vsockOffset; - in - { config, ... }: - { - services.openssh = { - enable = true; - settings = { - PermitRootLogin = "yes"; - PermitEmptyPasswords = "yes"; - }; + (mkIf config.sshBackdoor.enable { + services.openssh = { + enable = true; + settings = { + PermitRootLogin = "yes"; + PermitEmptyPasswords = "yes"; }; + }; - security.pam.services.sshd = { - allowNullPassword = true; - }; - - virtualisation.qemu.options = [ - "-device vhost-vsock-pci,guest-cid=${ - toString (config.virtualisation.test.nodeNumber + vsockOffset) - }" - ]; - } - )) + security.pam.services.sshd = { + allowNullPassword = true; + }; + }) ]; + nodeDefaults = mkIf config.sshBackdoor.enable ( + let + inherit (config.sshBackdoor) vsockOffset; + in + { config, ... }: + { + virtualisation.qemu.options = [ + "-device vhost-vsock-pci,guest-cid=${ + toString (config.virtualisation.test.nodeNumber + vsockOffset) + }" + ]; + } + ); + # Docs: nixos/doc/manual/development/writing-nixos-tests.section.md /** See https://nixos.org/manual/nixos/unstable#sec-override-nixos-test diff --git a/nixos/lib/testing/run.nix b/nixos/lib/testing/run.nix index e34e585241be..646832f71e62 100644 --- a/nixos/lib/testing/run.nix +++ b/nixos/lib/testing/run.nix @@ -2,6 +2,7 @@ config, hostPkgs, lib, + containers, options, ... }: @@ -96,12 +97,15 @@ in requiredSystemFeatures = [ "nixos-test" ] + # Containers use systemd-nspawn, which requires pid 0 inside of the sandbox. + ++ lib.optional (builtins.length (lib.attrNames containers) > 0) "uid-range" ++ lib.optional isLinux "kvm" ++ lib.optional isDarwin "apple-virt"; nativeBuildInputs = lib.optionals config.enableDebugHook [ hostPkgs.openssh hostPkgs.inetutils + hostPkgs.socat # to allow SSH backdoor connections for systemd-nspawn containers ]; buildCommand = '' diff --git a/nixos/lib/testing/testScript.nix b/nixos/lib/testing/testScript.nix index bde7b78607b4..4ce368c0b8db 100644 --- a/nixos/lib/testing/testScript.nix +++ b/nixos/lib/testing/testScript.nix @@ -56,11 +56,12 @@ in # reuse memoized config v ) config.nodesCompat; + containers = config.containers; } else config.testScript; - defaults = + nodeDefaults = { config, name, ... }: { # Make sure all derivations referenced by the test diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index fcae71655603..7ed613ef87f0 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1651,6 +1651,7 @@ ./services/web-apps/goatcounter.nix ./services/web-apps/gotify-server.nix ./services/web-apps/gotosocial.nix + ./services/web-apps/goupile.nix ./services/web-apps/grav.nix ./services/web-apps/grocy.nix ./services/web-apps/guacamole-client.nix diff --git a/nixos/modules/services/networking/nftables.nix b/nixos/modules/services/networking/nftables.nix index 7275d25ad5e1..40718c879ccf 100644 --- a/nixos/modules/services/networking/nftables.nix +++ b/nixos/modules/services/networking/nftables.nix @@ -298,7 +298,6 @@ in let enabledTables = lib.filterAttrs (_: table: table.enable) cfg.tables; deletionsScript = pkgs.writeScript "nftables-deletions" '' - #! ${pkgs.nftables}/bin/nft -f ${ if cfg.flushRuleset then "flush ruleset" @@ -313,9 +312,9 @@ in ${cfg.extraDeletions} ''; deletionsScriptVar = "/var/lib/nftables/deletions.nft"; + makeDeletions = "${pkgs.nftables}/bin/nft -f ${deletionsScriptVar}"; ensureDeletions = pkgs.writeShellScript "nftables-ensure-deletions" '' touch ${deletionsScriptVar} - chmod +x ${deletionsScriptVar} ''; saveDeletionsScript = pkgs.writeShellScript "nftables-save-deletions" '' cp ${deletionsScript} ${deletionsScriptVar} @@ -380,7 +379,7 @@ in saveDeletionsScript ]; ExecStop = [ - deletionsScriptVar + makeDeletions cleanupDeletionsScript ]; StateDirectory = "nftables"; diff --git a/nixos/modules/services/web-apps/goupile.nix b/nixos/modules/services/web-apps/goupile.nix new file mode 100644 index 000000000000..0cb986e1c7f4 --- /dev/null +++ b/nixos/modules/services/web-apps/goupile.nix @@ -0,0 +1,147 @@ +{ + lib, + pkgs, + config, + ... +}: +let + cfg = config.services.goupile; + settingsFormat = pkgs.formats.ini { }; +in +{ + options.services.goupile = { + enable = lib.mkEnableOption "Goupile server"; + package = lib.mkPackageOption pkgs "goupile" { }; + + enableSandbox = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Enable the sandbox option."; + }; + + settings = lib.mkOption { + type = lib.types.submodule { + freeformType = settingsFormat.type; + options = { + HTTP.Port = lib.mkOption { + type = lib.types.port; + default = 8889; + description = "The port goupile runs on"; + }; + Data.RootDirectory = lib.mkOption { + type = lib.types.str; + default = "/var/lib/goupile"; + description = "Goupile's data directory."; + }; + }; + }; + default = { }; # default will be lost for submodules if overriden + example = lib.literalExpression '' + { + HTTP.Port = 8888; + } + ''; + description = '' + The options for `systemd.services.goupile` in ini format. + + The configuration options available can be found here + https://github.com/Koromix/rygel/blob/goupile/3.11.1/src/goupile/server/admin.cc#L41 + ''; + }; + + configFile = lib.mkOption { + type = lib.types.path; + description = '' + The configuration file to be passed to goupile server. + + By default the configuration file is created from `services.goupile.settings`. + ''; + }; + + hostName = lib.mkOption { + type = lib.types.str; + default = "goupile"; + description = "Nginx service name for goupile service."; + }; + }; + config = lib.mkIf cfg.enable ( + lib.mkMerge [ + { + services.nginx = { + enable = lib.mkDefault true; + virtualHosts.${cfg.hostName} = { + locations."/".proxyPass = "http://${cfg.hostName}:${builtins.toString cfg.settings.HTTP.Port}"; + }; + }; + } + { + services.goupile.configFile = settingsFormat.generate "goupile.ini" cfg.settings; + } + { + systemd.services.goupile = { + wants = [ "network-online.target" ]; + after = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + + documentation = [ "https://goupile.org/en" ]; + description = "Goupile eCRF"; + + serviceConfig = { + ExecStart = '' + ${lib.getExe cfg.package} \ + ${lib.optionalString cfg.enableSandbox "--sandbox"} \ + -C ${cfg.configFile} + ''; + + DynamicUser = true; + + RuntimeDirectory = "goupile"; + RuntimeDirectoryPreserve = "yes"; + StateDirectory = "goupile"; + UMask = 0077; + WorkingDirectory = "%S/goupile"; + + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "~@privileged" + "~@resources" + "~@obsolete" + "~@mount" + "@system-service" + "@file-system" + "@basic-io" + "@clock" + ]; + + ProtectHome = true; + PrivateUsers = true; + PrivateDevices = true; + ProtectKernelLogs = true; + ProtectControlGroups = true; + ProtectKernelModules = true; + + CapabilityBoundingSet = [ + "CAP_SYS_PTRACE" + "CAP_CHOWN" + "CAP_DAC_OVERRIDE" + "CAP_FOWNER" + "CAP_KILL" # Required for child process management + "CAP_NET_BIND_SERVICE" + "CAP_SETGID" + "CAP_SETUID" + "CAP_SYS_CHROOT" + "CAP_SYS_RESOURCE" + ]; + + Restart = "always"; + RestartSec = 20; + TimeoutStopSec = 30; + LimitNOFILE = 4096; + }; + }; + } + ] + ); + + meta.maintainers = lib.teams.ngi.members; +} diff --git a/nixos/modules/testing/test-instrumentation.nix b/nixos/modules/testing/test-instrumentation.nix index 0a16df3dafd6..d697a5197272 100644 --- a/nixos/modules/testing/test-instrumentation.nix +++ b/nixos/modules/testing/test-instrumentation.nix @@ -86,7 +86,8 @@ in options.testing = { backdoor = lib.mkEnableOption "backdoor service in stage 2" // { - default = true; + # See assertion below for why the backdoor doesn't work with containers. + default = !config.boot.isContainer; }; initrdBackdoor = lib.mkEnableOption '' @@ -105,7 +106,20 @@ in { assertion = cfg.initrdBackdoor -> config.boot.initrd.systemd.enable; message = '' - testing.initrdBackdoor requires boot.initrd.systemd.enable to be enabled. + `testing.initrdBackdoor` requires `boot.initrd.systemd.enable` to be enabled. + ''; + } + { + assertion = config.boot.isContainer -> !cfg.backdoor; + message = '' + `testing.backdoor` uses virtio console, which does not work with + containers (we use `nsenter` instead). + ''; + } + { + assertion = config.boot.isContainer -> !cfg.initrdBackdoor; + message = '' + `testing.initrdBackdoor` does not work with containers as there is no initrd. ''; } ]; diff --git a/nixos/modules/virtualisation/guest-networking-options.nix b/nixos/modules/virtualisation/guest-networking-options.nix index 817ccc4e6370..39d9eea24f3d 100644 --- a/nixos/modules/virtualisation/guest-networking-options.nix +++ b/nixos/modules/virtualisation/guest-networking-options.nix @@ -71,7 +71,7 @@ in virtualisation.vlans = lib.mkOption { type = types.listOf types.ints.unsigned; default = if cfg.interfaces == { } then [ 1 ] else [ ]; - defaultText = lib.literalExpression "if cfg.interfaces == {} then [ 1 ] else [ ]"; + defaultText = lib.literalExpression "if config.virtualisation.interfaces == {} then [ 1 ] else [ ]"; example = [ 1 2 diff --git a/nixos/modules/virtualisation/nspawn-container/default.nix b/nixos/modules/virtualisation/nspawn-container/default.nix index 6f303eec16df..f56167ca0b07 100644 --- a/nixos/modules/virtualisation/nspawn-container/default.nix +++ b/nixos/modules/virtualisation/nspawn-container/default.nix @@ -70,6 +70,45 @@ in config = { boot.isNspawnContainer = true; + assertions = [ + { + assertion = config.specialisation == { }; + message = '' + Setting 'specialisation' is disallowed for systemd-nspawn container configurations. + Activating a specialisation requires creating SUID wrappers (e.g., for 'sudo'), + which is prohibited within the Nix build sandbox where the test is run. + ''; + } + { + # Check every interface defined in allInterfaces. + # Containers try to create a bridge "${config.system.name}-${interfaceName}" + assertion = lib.all ( + iface: + let + hostName = "${config.system.name}-${iface.name}"; + in + lib.stringLength hostName <= 15 + ) (lib.attrValues cfg.allInterfaces); + + message = + let + offendingInterfaces = lib.filter ( + iface: lib.stringLength "${config.system.name}-${iface.name}" > 15 + ) (lib.attrValues cfg.allInterfaces); + offenderList = map ( + i: + "${config.system.name}-${i.name} (${toString (lib.stringLength "${config.system.name}-${i.name}")} chars)" + ) offendingInterfaces; + in + '' + The following generated host interface names exceed the Linux 15-character limit: + ${lib.concatStringsSep "\n " offenderList} + + Please shorten 'config.system.name' or the interface names in 'virtualisation.interfaces'. + ''; + } + ]; + # TODO(arianvp): Remove after https://github.com/NixOS/nixpkgs/pull/480686 is merged console.enable = true; @@ -94,6 +133,9 @@ in # > kind of unit allocation or registration with systemd-machined. "--keep-unit" "--register=no" + + # Send a READY=1 notification to a socket when the container is fully booted. + "--notify-ready=yes" ]; system.build.nspawn = diff --git a/nixos/modules/virtualisation/nspawn-container/run-nspawn/src/run_nspawn/__init__.py b/nixos/modules/virtualisation/nspawn-container/run-nspawn/src/run_nspawn/__init__.py index 99f50038fd7c..3eb622cbbfe8 100644 --- a/nixos/modules/virtualisation/nspawn-container/run-nspawn/src/run_nspawn/__init__.py +++ b/nixos/modules/virtualisation/nspawn-container/run-nspawn/src/run_nspawn/__init__.py @@ -68,7 +68,9 @@ def ensure_vlan_bridge(vlan: int) -> typing.Generator[str, None, None]: ipv6_addr = f"2001:db8:{vlan}::fe/64" bridge_name = f"br{vlan}" + tap_name = f"vde-tap{vlan}" bridge_path = Path("/sys/class/net") / bridge_name + tap_path = Path("/sys/class/net") / tap_name try: # To avoid racing against other nspawn containers that also # need this vlan, grab an exclusive lock. @@ -80,6 +82,19 @@ def ensure_vlan_bridge(vlan: int) -> typing.Generator[str, None, None]: run_ip("addr", "add", ipv4_addr, "dev", bridge_name) run_ip("addr", "add", ipv6_addr, "dev", bridge_name) + if tap_path.exists(): + logger.info(f"attaching {tap_name} to {bridge_name}") + run_ip("link", "set", tap_name, "master", bridge_name) + run_ip("link", "set", tap_name, "up") + else: + logger.warning( + f"TAP {tap_name} not found; container will be isolated from VDE" + ) + if not Path("/dev/net").exists(): + logger.warning( + "A common reason for this is that /dev/net is not available in the Nix sandbox. Try adding /dev/net to extra-sandbox-paths." + ) + yield bridge_name finally: # To avoid racing against other nspawn containers that also @@ -126,6 +141,7 @@ def mk_veth( def run( container_name: str, root_dir_str: str, + shared_dir_str: typing.Optional[str], interfaces: dict, nspawn_options: list[str], init: str, @@ -166,12 +182,19 @@ def run( flush=True, ) + shared_dir = Path(shared_dir_str) if shared_dir_str else None + cp = subprocess.Popen( [ "@systemd-nspawn@", *nspawn_options, f"--directory={root_dir}", f"--network-namespace-path={netns.path}", + *( + [f"--bind={shared_dir}:/tmp/shared"] + if shared_dir is not None + else [] + ), init, *cmdline, ], @@ -218,6 +241,11 @@ def main(): required=True, help="Path to container root directory (overridable with RUN_NSPAWN_ROOT_DIR)", ) + arg_parser.add_argument( + "--shared-dir", + required=False, + help="Path to a shared directory to bind-mount into the container at /tmp/shared (overridable with RUN_NSPAWN_SHARED_DIR)", + ) arg_parser.add_argument( "--interfaces-json", dest="interfaces", @@ -239,6 +267,7 @@ def main(): run( container_name=args.container_name, root_dir_str=os.getenv("RUN_NSPAWN_ROOT_DIR", default=args.root_dir), + shared_dir_str=os.getenv("RUN_NSPAWN_SHARED_DIR", default=args.shared_dir), interfaces=args.interfaces, nspawn_options=nspawn_options, init=args.init, diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index de66d01e1c09..0e9ae55b4f16 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -168,6 +168,7 @@ in node-name = runTest ./nixos-test-driver/node-name.nix; busybox = runTest ./nixos-test-driver/busybox.nix; console-log = runTest ./nixos-test-driver/console-log.nix; + containers = runTest ./nixos-test-driver/containers.nix; driver-timeout = pkgs.runCommand "ensure-timeout-induced-failure" { @@ -689,6 +690,7 @@ in gotenberg = runTest ./gotenberg.nix; gotify-server = runTest ./gotify-server.nix; gotosocial = runTest ./web-apps/gotosocial.nix; + goupile = runTest ./web-apps/goupile; grafana = handleTest ./grafana { }; graphite = runTest ./graphite.nix; grav = runTest ./web-apps/grav.nix; @@ -1137,9 +1139,6 @@ in nixos-rebuild-target-host = runTest { imports = [ ./nixos-rebuild-target-host.nix ]; }; - nixos-rebuild-target-host-interrupted = runTest { - imports = [ ./nixos-rebuild-target-host-interrupted.nix ]; - }; nixpkgs = pkgs.callPackage ../modules/misc/nixpkgs/test.nix { inherit evalMinimalConfig; }; nixpkgs-config-allow-unfree = pkgs.callPackage ../modules/misc/nixpkgs/test-nixpkgs-config-allow-unfree.nix @@ -1648,6 +1647,7 @@ in teleports = runTest ./teleports.nix; temporal = runTest ./temporal.nix; terminal-emulators = handleTest ./terminal-emulators.nix { }; + test-containers-bittorrent = runTest ./test-containers-bittorrent.nix; thanos = runTest ./thanos.nix; thelounge = handleTest ./thelounge.nix { }; tiddlywiki = runTest ./tiddlywiki.nix; diff --git a/nixos/tests/nixos-rebuild-target-host-interrupted.nix b/nixos/tests/nixos-rebuild-target-host-interrupted.nix deleted file mode 100644 index 7ddb660c3199..000000000000 --- a/nixos/tests/nixos-rebuild-target-host-interrupted.nix +++ /dev/null @@ -1,232 +0,0 @@ -{ hostPkgs, ... }: - -# This test recreates a remote deployment scenario where the connection -# between deployer and target is closed during the deployment - in this -# case because the connection goes over a 'reverse ssh' tunnel service -# that has changes that are being deployed. - -# This is not seamless (the deployer doesn't get to see the logs after -# the disconnect), but is a lot better than the old behaviour, where -# the switch was aborted and the connection never restored. - -{ - name = "nixos-rebuild-target-host-interrupted"; - - # TODO: remove overlay from nixos/modules/profiles/installation-device.nix - # make it a _small package instead, then remove pkgsReadOnly = false;. - node.pkgsReadOnly = false; - - nodes = { - deployer = - { - nodes, - lib, - pkgs, - ... - }: - let - inherit (import ./ssh-keys.nix pkgs) snakeOilPrivateKey snakeOilPublicKey; - in - { - imports = [ - ../modules/profiles/installation-device.nix - ]; - - nix.settings = { - substituters = lib.mkForce [ ]; - hashed-mirrors = null; - connect-timeout = 1; - }; - - system.includeBuildDependencies = true; - - virtualisation = { - cores = 2; - memorySize = 3072; - }; - - services.openssh.enable = true; - users.users.root.openssh.authorizedKeys.keys = [ nodes.target.system.build.publicKey ]; - system.extraDependencies = [ - # so that it doesn't need to be built inside the test - pkgs.nixVersions.latest - ]; - - system.build.privateKey = snakeOilPrivateKey; - system.build.publicKey = snakeOilPublicKey; - system.switch.enable = true; - - services.getty.autologinUser = lib.mkForce "root"; - }; - - target = - { - nodes, - lib, - pkgs, - ... - }: - let - inherit (import ./ssh-keys.nix pkgs) snakeOilPrivateKey snakeOilPublicKey; - targetConfig = { - documentation.enable = false; - services.openssh.enable = true; - system.build.privateKey = snakeOilPrivateKey; - system.build.publicKey = snakeOilPublicKey; - - users.users.root.openssh.authorizedKeys.keys = [ nodes.deployer.system.build.publicKey ]; - users.users.alice.openssh.authorizedKeys.keys = [ nodes.deployer.system.build.publicKey ]; - users.users.bob.openssh.authorizedKeys.keys = [ nodes.deployer.system.build.publicKey ]; - - users.users.alice.extraGroups = [ "wheel" ]; - users.users.bob.extraGroups = [ "wheel" ]; - - # Disable sudo for root to ensure sudo isn't called without `--sudo` - security.sudo.extraRules = lib.mkForce [ - { - groups = [ "wheel" ]; - commands = [ { command = "ALL"; } ]; - } - { - users = [ "alice" ]; - commands = [ - { - command = "ALL"; - options = [ "NOPASSWD" ]; - } - ]; - } - ]; - - nix.settings.trusted-users = [ "@wheel" ]; - - services.autossh-ng.sessions.will-be-interrupted-by-rebuild = { - user = "root"; - destination = "deployer"; - extraArguments = "-R2222:localhost:22"; - hostKeyChecking = false; - }; - }; - in - { - imports = [ ./common/user-account.nix ]; - - config = lib.mkMerge [ - targetConfig - { - system.build = { - inherit targetConfig; - }; - system.switch.enable = true; - - networking.hostName = "target"; - } - ]; - }; - }; - - testScript = - { nodes, ... }: - let - sshConfig = builtins.toFile "ssh.conf" '' - UserKnownHostsFile=/dev/null - StrictHostKeyChecking=no - ''; - - targetConfigJSON = hostPkgs.writeText "target-configuration.json" ( - builtins.toJSON nodes.target.system.build.targetConfig - ); - - targetNetworkJSON = hostPkgs.writeText "target-network.json" ( - builtins.toJSON nodes.target.system.build.networkConfig - ); - - configFile = - hostname: - hostPkgs.writeText "configuration.nix" # nix - '' - { lib, pkgs, modulesPath, ... }: { - imports = [ - (modulesPath + "/virtualisation/qemu-vm.nix") - (modulesPath + "/testing/test-instrumentation.nix") - (modulesPath + "/../tests/common/user-account.nix") - (lib.modules.importJSON ./target-configuration.json) - (lib.modules.importJSON ./target-network.json) - ./hardware-configuration.nix - ]; - - boot.loader.grub = { - enable = true; - device = "/dev/vda"; - forceInstall = true; - }; - - # needed to make NIX_SSHOPTS work for nix-copy-closure - # 2.31.3 (current default) break, 2.32.6 and 2.33.3 (current latest) work - # let's use the default here again once the fix has made it there. - nix.package = pkgs.nixVersions.latest; - - # We're changing the '-E' parameter to the new hostname here, - # not because we care about the logs, but because we want to - # force the scenario where the connection is broken during the - # deployment (because the autossh-ng service is stopped and - # started): - services.autossh-ng.sessions.will-be-interrupted-by-rebuild.extraArguments = "-R2222:localhost:22 -E ${hostname}"; - - # this will be asserted to validate the switch happened: - networking.hostName = "${hostname}"; - } - ''; - in - # python - '' - start_all() - target.wait_for_open_port(22) - - deployer.wait_until_succeeds("ping -c1 target") - deployer.succeed("install -Dm 600 ${nodes.deployer.system.build.privateKey} ~root/.ssh/id_ecdsa") - deployer.succeed("install ${sshConfig} ~root/.ssh/config") - - target.succeed("nixos-generate-config") - target.succeed("install -Dm 600 ${nodes.target.system.build.privateKey} ~root/.ssh/id_ecdsa") - deployer.succeed("scp alice@target:/etc/nixos/hardware-configuration.nix /root/hardware-configuration.nix") - target.wait_for_unit("autossh-ng-will-be-interrupted-by-rebuild.service") - - deployer.copy_from_host("${configFile "config-1-deployed"}", "/root/configuration-1.nix") - deployer.copy_from_host("${configFile "config-2-deployed"}", "/root/configuration-2.nix") - deployer.copy_from_host("${targetNetworkJSON}", "/root/target-network.json") - deployer.copy_from_host("${targetConfigJSON}", "/root/target-configuration.json") - - with subtest("Deploy to alice@target via reverse ssh"): - deployer.wait_for_unit("multi-user.target") - # Uses TTY/send_chars instead of deployer.succeed to set NIX_SSHOPTS - deployer.send_chars("NIX_SSHOPTS=\"-p 2222\" nixos-rebuild switch -I nixos-config=/root/configuration-1.nix --target-host alice@localhost --sudo\n") - - # the connection breaks, but the 'switch' should now continue in the background: - deployer.wait_until_tty_matches("1", "error: while running command with remote sudo") - - def deployed(last_try: bool) -> bool: - target_hostname = deployer.succeed("ssh alice@target cat /etc/hostname", timeout=20).rstrip() - if last_try: - print(f"Still seeing hostname {target_hostname}") - return target_hostname == "config-1-deployed" - retry(deployed) - - with subtest("Deploy to bob@target via reverse ssh with password-based sudo"): - deployer.wait_for_unit("multi-user.target") - # Uses TTY/send_chars instead of deployer.succeed to set NIX_SSHOPTS and for ask-sudo-password - deployer.send_chars("NIX_SSHOPTS=\"-p 2222\" nixos-rebuild switch -I nixos-config=/root/configuration-2.nix --target-host bob@localhost --ask-sudo-password\n") - deployer.wait_until_tty_matches("1", "password for bob") - deployer.send_chars("${nodes.target.users.users.bob.password}\n") - - # the connection breaks, but the 'switch' should now continue in the background: - deployer.wait_until_tty_matches("1", "error: while running command with remote sudo") - - def deployed(last_try: bool) -> bool: - target_hostname = deployer.succeed("ssh alice@target cat /etc/hostname", timeout=20).rstrip() - if last_try: - print(f"Still seeing hostname {target_hostname}") - return target_hostname == "config-2-deployed" - retry(deployed) - ''; -} diff --git a/nixos/tests/nixos-test-driver/containers.nix b/nixos/tests/nixos-test-driver/containers.nix new file mode 100644 index 000000000000..073e2de28bd6 --- /dev/null +++ b/nixos/tests/nixos-test-driver/containers.nix @@ -0,0 +1,77 @@ +{ pkgs, ... }: +{ + name = "containers"; + meta.maintainers = with pkgs.lib.maintainers; [ jfly ]; + + nodes = { + n1 = { + virtualisation.vlans = [ 1 ]; + }; + n2 = { + virtualisation.vlans = [ + 2 + ]; + }; + }; + + containers = { + c1 = { + virtualisation.vlans = [ 1 ]; + }; + c2 = { + virtualisation.vlans = [ 2 ]; + }; + c12 = { + virtualisation.vlans = [ + 1 + 2 + ]; + }; + }; + + testScript = /* python */ '' + c1.start() + c2.start() + c12.start() + + c1.succeed("echo hello > /hello.txt") + c1.copy_from_machine("/hello.txt") + + c1.systemctl("start network-online.target") + c2.systemctl("start network-online.target") + c12.systemctl("start network-online.target") + c1.wait_for_unit("network-online.target") + c2.wait_for_unit("network-online.target") + c12.wait_for_unit("network-online.target") + + # Confirm containers in vlan 1 can talk to each other. + c1.succeed("ping -c 1 c12") + c12.succeed("ping -c 1 c1") + + # Confirm containers in vlan 2 can talk to each other. + c2.succeed("ping -c 1 c12") + c12.succeed("ping -c 1 c2") + + # Confirm containers in separate vlans cannot talk to each other. + c1.fail("ping -c 1 -W 1 c2") + + n1.start() + n2.start() + n1.systemctl("start network-online.target") + n2.systemctl("start network-online.target") + n1.wait_for_unit("network-online.target") + n2.wait_for_unit("network-online.target") + + # Confirm containers and nodes in the same vlan can talk to each other. + c1.succeed("ping -c 1 n1") + n1.succeed("ping -c 1 c1") + c2.succeed("ping -c 1 n2") + n2.succeed("ping -c 1 c2") + + # Confirm containers and nodes in different vlans cannot talk to each other. + c1.fail("ping -c 1 -W 1 n2") + n1.fail("ping -c 1 -W 1 c2") + c2.fail("ping -c 1 -W 1 n1") + n2.fail("ping -c 1 -W 1 c1") + ''; +} diff --git a/nixos/tests/test-containers-bittorrent.nix b/nixos/tests/test-containers-bittorrent.nix new file mode 100644 index 000000000000..aeac191cdc2e --- /dev/null +++ b/nixos/tests/test-containers-bittorrent.nix @@ -0,0 +1,215 @@ +# This test runs a Bittorrent tracker on one machine, and verifies +# that two client machines can download the torrent using +# `aria2c'. The first client (behind a NAT router) downloads +# from the initial seeder running on the tracker. Then we kill the +# initial seeder. The second client downloads from the first client, +# which only works if the first client successfully uses the UPnP-IGD +# protocol to poke a hole in the NAT. + +# We use aria2 as the initial seeder because transmission +# fails in the sandbox because of systemd hardening settings, +# namely MountAPIVFS=yes, so we get the following error: + +# $ journalctl --unit transmission.service +# (n-daemon)[417]: transmission.service: Failed to create destination mount point node '/run/transmission/run/host/.os-release-stage/', ignoring: Read-only file system +# (n-daemon)[417]: transmission.service: Failed to mount /run/systemd/propagate/.os-release-stage to /run/transmission/run/host/.os-release-stage/: No such file or directory +# (n-daemon)[417]: transmission.service: Failed to set up mount namespacing: /run/host/.os-release-stage/: No such file or directory +# (n-daemon)[417]: transmission.service: Failed at step NAMESPACE spawning /nix/store/zfksw9bllp95pl45d1nxmpd2lks42bkj-transmission-4.0.6/bin/transmission-daemon: No such file or directory +# systemd[1]: transmission.service: Main process exited, code=exited, status=226/NAMESPACE + +{ lib, hostPkgs, ... }: + +let + + # Some random file to serve. + file = hostPkgs.hello.src; + + internalRouterAddress = "192.168.3.1"; + internalClient1Address = "192.168.3.2"; + + # cannot use documentation networks (198.51.100.0/24 or 192.0.2.0/24) here + # because miniupnpd recognizes them as such and refuses to work with them + # https://github.com/miniupnp/miniupnp/blob/2a74cb2f27cacf06d2b50c187e8f90aa1f5c2528/miniupnpd/miniupnpd.c#L998 + externalRouterAddress = "80.100.100.1"; + externalClient2Address = "80.100.100.2"; + externalTrackerAddress = "80.100.100.3"; + + download-dir = "/tmp/aria2-downloads"; + peerConfig = + { pkgs, ... }: + { + environment.systemPackages = [ + pkgs.aria2 + pkgs.transmission_4 # only needed for transmission-create + ]; + }; +in + +{ + name = "bittorrent"; + meta = { + maintainers = [ + lib.maintainers.kmein + ]; + }; + + containers = { + tracker = + { pkgs, ... }: + { + imports = [ peerConfig ]; + + virtualisation.vlans = [ 1 ]; + networking.firewall.enable = false; + networking.interfaces.eth1.ipv4.addresses = [ + { + address = externalTrackerAddress; + prefixLength = 24; + } + ]; + + # We need Apache on the tracker to serve the torrents. + services.httpd = { + enable = true; + virtualHosts = { + "torrentserver.org" = { + adminAddr = "foo@example.org"; + documentRoot = "/tmp"; + }; + }; + }; + services.opentracker.enable = true; + }; + + router = + { pkgs, containers, ... }: + { + virtualisation.vlans = [ + 1 + 2 + ]; + networking.nat.enable = true; + networking.nat.internalInterfaces = [ "eth2" ]; + networking.nat.externalInterface = "eth1"; + networking.firewall.enable = true; + networking.firewall.trustedInterfaces = [ "eth2" ]; + networking.interfaces.eth0.ipv4.addresses = [ ]; + networking.interfaces.eth1.ipv4.addresses = [ + { + address = externalRouterAddress; + prefixLength = 24; + } + ]; + networking.interfaces.eth2.ipv4.addresses = [ + { + address = internalRouterAddress; + prefixLength = 24; + } + ]; + networking.nftables.enable = true; + services.miniupnpd = { + enable = true; + externalInterface = "eth1"; + internalIPs = [ "eth2" ]; + appendConfig = '' + ext_ip=${externalRouterAddress} + ''; + }; + }; + + client1 = + { pkgs, containers, ... }: + { + imports = [ peerConfig ]; + environment.systemPackages = [ pkgs.miniupnpc ]; + + virtualisation.vlans = [ 2 ]; + networking.interfaces.eth0.ipv4.addresses = [ ]; + networking.interfaces.eth1.ipv4.addresses = [ + { + address = internalClient1Address; + prefixLength = 24; + } + ]; + networking.defaultGateway = internalRouterAddress; + networking.firewall.enable = false; + }; + + client2 = + { pkgs, ... }: + { + imports = [ peerConfig ]; + + virtualisation.vlans = [ 1 ]; + networking.interfaces.eth0.ipv4.addresses = [ ]; + networking.interfaces.eth1.ipv4.addresses = [ + { + address = externalClient2Address; + prefixLength = 24; + } + ]; + networking.firewall.enable = false; + }; + }; + + testScript = + { containers, ... }: + '' + start_all() + + # Wait for network and miniupnpd. + router.systemctl("start network-online.target") + router.wait_for_unit("network-online.target") + router.wait_for_unit("miniupnpd") + + # Create the torrent. + tracker.succeed("mkdir -p ${download-dir}") + tracker.succeed( + "cp ${file} ${download-dir}/test.tar.bz2" + ) + tracker.succeed( + "transmission-create ${download-dir}/test.tar.bz2 --private --tracker http://${externalTrackerAddress}:6969/announce --outfile /tmp/test.torrent" + ) + tracker.succeed("chmod 644 /tmp/test.torrent") + + # Start the tracker + tracker.systemctl("start network-online.target") + tracker.wait_for_unit("network-online.target") + tracker.wait_for_unit("opentracker.service") + tracker.wait_for_open_port(6969) + + # --- Start the initial seeder using aria2 --- + # https://stackoverflow.com/a/44528978 + tracker.execute( + "aria2c --enable-dht=false --seed-time=999 --dir=${download-dir} " + "-V --seed-ratio=0.0 " + "/tmp/test.torrent >/dev/null &" + ) + + # --- Wait until the tracker shows we are seeding --- + tracker.wait_until_succeeds("curl -s http://localhost:6969/stats | grep -q 'serving 1 torrents'") + + # Now we should be able to download from the client behind the NAT. + tracker.wait_for_unit("httpd") + + def connect_from(machine): + machine.systemctl("start network-online.target") + machine.wait_for_unit("network-online.target") + machine.execute( + "aria2c --enable-dht=false --seed-time=999 --dir=${download-dir} " + "http://${externalTrackerAddress}/test.torrent >/dev/null &" + ) + machine.wait_until_succeeds( + "cmp ${download-dir}/test.tar.bz2 ${file}" + ) # Wait for download to finish and verify + + connect_from(client1) + + # --- Bring down the initial seeder --- + tracker.succeed("pkill aria2c") + + # Now download from the second client. This can only succeed if + # the first client created a NAT hole in the router. + connect_from(client2) + ''; +} diff --git a/nixos/tests/web-apps/goupile/basic_interaction_test.py b/nixos/tests/web-apps/goupile/basic_interaction_test.py new file mode 100644 index 000000000000..46b0f78903fc --- /dev/null +++ b/nixos/tests/web-apps/goupile/basic_interaction_test.py @@ -0,0 +1,178 @@ +import os +import openpyxl +import tempfile +from playwright.sync_api import sync_playwright + +BASE_URL = "http://localhost:8889" + +# NOTE: these are the passwords +ADMIN_PASSWD = "car-shop-in-the-mall" +ALICE_PASSWD = "user-goes-to-the-car-shop" + + +def run_test(): + is_headful = os.getenv("HEADFUL") == "1" + + with sync_playwright() as p: + browser = p.chromium.launch(headless=not is_headful) + context = browser.new_context( + accept_downloads=True, record_video_dir="/tmp/videos/" + ) + # more default timeout for slow nixos test vms + context.set_default_timeout(90 * 1000) + page = context.new_page() + + page.goto(f"{BASE_URL}/admin") + + # admin and doman setup + page.get_by_role("textbox", name="Domain name *").fill("domain") + page.get_by_role("textbox", name="Domain title *").fill("domain") + page.get_by_role("textbox", name="Password *").fill(ADMIN_PASSWD) + page.get_by_role("textbox", name="Confirmation").fill(ADMIN_PASSWD) + page.get_by_role("textbox", name="Decryption key *").click() + page.get_by_role("button", name="Installer").click() + + # login to admin dashboard as admin + page.get_by_role("textbox", name="Username *").fill("admin") + page.get_by_role("textbox", name="Password *").fill(ADMIN_PASSWD) + page.get_by_role("button", name="Login").click() + + # create a sample project, it will switch the view to project's configure page + page.get_by_text("Create new project").click() + page.get_by_role("textbox", name="Name *").fill("proj1") + page.get_by_role("button", name="Create").click() + + # create a test non-root user, alice + page.get_by_text("Create new user").click() + page.get_by_role("textbox", name="Username *").fill("alice") + page.get_by_role("button", name="No", exact=True).click() + page.get_by_role("textbox", name="Password *").fill(ALICE_PASSWD) + page.get_by_role("textbox", name="Confirmation").fill(ALICE_PASSWD) + page.get_by_role("button", name="Create").click() + + # give alice, permissions to access the project + page.get_by_role("button", name="Assign").nth(1).click() + page.get_by_text("Read", exact=True).click() + page.get_by_text("Save", exact=True).click() + page.get_by_text("Export", exact=True).click() + page.get_by_text("Download", exact=True).click() + + # Open the project in new page + page.locator("form").get_by_role("button", name="Edit").click() + with page.expect_popup() as page1_info: + page.get_by_role("link", name="access").click() + page1 = page1_info.value + page1.set_default_timeout(120 * 1000) + + # fill entries as admin (enter 1 for everything) + page1.get_by_role("button", name="Create new record").click() + + page1.locator("#ins_tiles").get_by_text("Introduction").click() + page1.get_by_role("textbox", name="Inclusion date *").fill("2000-01-01") + page1.get_by_role("spinbutton", name="Age *").click() + page1.get_by_role("spinbutton", name="Age *").fill("1") + page1.get_by_role("button", name="Save").click() + page1.wait_for_timeout(1000) + + page1.get_by_role("button", name="Advanced").click() + page1.get_by_role("spinbutton", name="Age *").click() + page1.get_by_role("spinbutton", name="Age *").fill("1") + page1.get_by_role("button", name="Save").click() + page1.wait_for_timeout(1000) + + page1.get_by_role("button", name="Page layout").click() + page1.get_by_role("spinbutton", name="Variable A1").fill("1") + page1.get_by_role("button", name="Save").click() + page1.wait_for_timeout(1000) + + # create export #1 + page1.get_by_role("button", name="Data").click() + page1.wait_for_timeout(1000) + + page1.get_by_role("button", name="Data exports").click() + with page1.expect_download() as download_info: + page1.get_by_role("button", name="Create export").click() + + # logout as admin + page.get_by_role("button", name="admin", exact=True).click() + with page.expect_popup() as page2_info: + page.get_by_role("link", name="access").click() + page2 = page2_info.value + page2.set_default_timeout(120 * 1000) + + page2.get_by_role("button", name="admin").click() + page2.get_by_role("button", name="Logout").click() + + # login as alice + page2.get_by_role("textbox", name="Username *").fill("alice") + page2.get_by_role("textbox", name="Password *").fill(ALICE_PASSWD) + page2.get_by_role("button", name="Login").click() + + # create entry as alice (fill `2` for everything) + page2.get_by_role("button", name="Create new record").click() + + page2.get_by_text("1 Introduction").click() + page2.get_by_role("textbox", name="Inclusion date *").fill("2000-01-01") + page2.get_by_role("spinbutton", name="Age *").click() + page2.get_by_role("spinbutton", name="Age *").fill("2") + page2.get_by_role("button", name="Save").click() + page2.wait_for_timeout(1000) + + page2.get_by_role("button", name="Advanced").click() + page2.get_by_role("spinbutton", name="Age *").click() + page2.get_by_role("spinbutton", name="Age *").fill("2") + page2.get_by_role("button", name="Save").click() + page2.wait_for_timeout(1000) + + page2.get_by_role("button", name="Page layout").click() + page2.get_by_role("spinbutton", name="Variable A1").click() + page2.get_by_role("spinbutton", name="Variable A1").fill("2") + page2.get_by_role("button", name="Save").click() + page2.wait_for_timeout(1000) + + # create export #2 + page2.get_by_role("button", name="Data").click() + page2.wait_for_timeout(1000) + + page2.get_by_role("button", name="Data exports").click() + page2.get_by_role("button", name="Previous exports").click() + + with page2.expect_download() as download1_info: + page2.locator("a").filter(has_text="Download").click() + + download1 = download1_info.value + save_path1 = os.path.join(tempfile.gettempdir(), download1.suggested_filename) + download1.save_as(save_path1) + + print(f"exported all records to {save_path1}") + + page2.get_by_role("button", name="Data exports").click() + with page2.expect_download() as download2_info: + page2.get_by_role("button", name="Create export").click() + + download2 = download2_info.value + save_path2 = os.path.join(tempfile.gettempdir(), download2.suggested_filename) + download2.save_as(save_path2) + + print(f"exported all records to {save_path2}") + + context.close() + browser.close() + + # check that exported files have correct entries + + wb1 = openpyxl.load_workbook(save_path1) + for sheet, cell in zip(["intro", "advanced", "layout"], ["D2", "D2", "C2"]): + val = wb1[sheet][cell].value + assert val == 1, f"Sheet {sheet}, Cell {cell}: Expected 1 (admin), got {val}" + + wb2 = openpyxl.load_workbook(save_path2) + for sheet, cell in zip(["intro", "advanced", "layout"], ["D3", "D3", "C3"]): + val = wb2[sheet][cell].value + assert val == 2, f"Sheet {sheet}, Cell {cell}: Expected 2 (alice), got {val}" + + print("Test passed successfully!") + + +if __name__ == "__main__": + run_test() diff --git a/nixos/tests/web-apps/goupile/default.nix b/nixos/tests/web-apps/goupile/default.nix new file mode 100644 index 000000000000..48215af9fa45 --- /dev/null +++ b/nixos/tests/web-apps/goupile/default.nix @@ -0,0 +1,153 @@ +{ + lib, + pkgs, + ... +}: +let + python = pkgs.python3.withPackages ( + ps: with ps; [ + requests + playwright + openpyxl + ] + ); + + runScript = "${lib.getExe python} ${./basic_interaction_test.py}"; + + run-goupile-test = pkgs.writeShellScriptBin "run-goupile-test" '' + set -euo pipefail + + export PLAYWRIGHT_BROWSERS_PATH=${pkgs.playwright-driver.browsers} + export PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 + + # check if attached to a terminal + if [ -t 1 ]; then + # interactive testing + export HEADFUL=''${HEADFUL:-1} + export PWDEBUG=''${PWDEBUG:-0} + export DISPLAY=''${DISPLAY:-:0} + if [ "$(id -u)" = "0" ] && [ -d "/home/alice" ]; then + runuser -u alice \ + -w DISPLAY,HEADFUL,PWDEBUG,PLAYWRIGHT_BROWSERS_PATH,PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD \ + -- ${runScript} + else + ${runScript} + fi + else + # non-interactive nixos test + + # Print instructions to the nix logs + cat <<'EOF' | tee >(systemd-cat -t goupile-e2e) + ================================================================================ + NOTE: The goupile e2e test can be run interactively either inside the vm or on the host + - First, run `nix-build -A nixosTests.goupile.driverInteractive` and `./result/bin/nixos-test-driver` + - Run `start_all()` inside the repl + - Then `$(nix-build -A nixosTests.goupile.interactive-script)/bin/run-goupile-test` to run the full test interactively + - Or `env PWDEBUG=1 $(nix-build -A nixosTests.goupile.interactive-script)/bin/run-goupile-test` to show the playwright inspector to debug + ================================================================================ + EOF + + echo "Starting smoke test..." | systemd-cat -t goupile-e2e + ${runScript} 2>&1 | tee >(systemd-cat -t goupile-e2e) + fi + ''; +in +{ + name = "goupile"; + + passthru.interactive-script = run-goupile-test; + + nodes.machine = + { + lib, + pkgs, + config, + ... + }: + { + services.goupile = { + enable = true; + enableSandbox = true; + settings.HTTP.Port = 8889; + }; + #systemd.services.goupile.environment.DEFAULT_SECCOMP_ACTION = "Log"; # Block|Log|Kill + networking = { + firewall.allowedTCPPorts = [ config.services.nginx.defaultHTTPListenPort ]; + hostName = "goupile"; + domain = "local"; + }; + + # goupile tries to resolve it at runtime, resolve it instead of patching it out + # as the dns resolution step serves a purpose, to force glibc to load NSS libraries + # see server/goupile.cc and search for getaddrinfo or www.example.com + networking.extraHosts = '' + 127.0.0.1 www.example.com + ''; + + environment.systemPackages = [ + python + run-goupile-test + ]; + + # more cores and memory to improve chromium performance + virtualisation.memorySize = lib.mkForce 8192; + virtualisation.cores = 4; + }; + + testScript = + { nodes, ... }: + let + port = builtins.toString nodes.machine.services.goupile.settings.HTTP.Port; + in + # py + '' + import os + start_all() + + machine.wait_for_unit("goupile.service") + machine.wait_for_open_port(${port}) + + machine.succeed("curl -q http://localhost:${port}") + machine.succeed("curl -q http://goupile.local") + machine.succeed("curl -q http://localhost") + + machine.succeed("run-goupile-test") + out_dir = os.environ.get("out", os.getcwd()) + machine.copy_from_vm("/tmp/videos", out_dir) + ''; + + # Debug interactively with: + # - nix-build -A nixosTests.goupile.driverInteractive + # - ./result/bin/nixos-test-driver + # - run_tests() + # ssh -o User=root vsock%3 (can also do vsock/3, but % works with scp etc.) + interactive.sshBackdoor.enable = true; + + interactive.nodes.machine = + { config, ... }: + let + port = config.services.goupile.settings.HTTP.Port; + in + { + imports = [ + # enable graphical session + users (alice, bob) + ../../common/x11.nix + ../../common/user-account.nix + ]; + services.xserver.enable = true; + test-support.displayManager.auto.user = "alice"; + + virtualisation.forwardPorts = [ + { + from = "host"; + host.port = port; + guest.port = port; + } + ]; + + # forwarded ports need to be accessible + networking.firewall.allowedTCPPorts = [ port ]; + }; + + meta.maintainers = lib.teams.ngi.members; +} diff --git a/pkgs/applications/editors/vscode/extensions/default.nix b/pkgs/applications/editors/vscode/extensions/default.nix index 5109e3c4a995..953b10c627bc 100644 --- a/pkgs/applications/editors/vscode/extensions/default.nix +++ b/pkgs/applications/editors/vscode/extensions/default.nix @@ -3008,8 +3008,8 @@ let mktplcRef = { publisher = "mesonbuild"; name = "mesonbuild"; - version = "1.28.1"; - hash = "sha256-Cu2sBg8wTjGLOMF4bCOG8noXZXZB2j5wSXZS2VxxNoA="; + version = "1.28.2"; + hash = "sha256-Wb3cfATe8pc+LftmKyFj3q6kmdTHUMtoIHlChKKeEoU="; }; meta = { changelog = "https://marketplace.visualstudio.com/items/mesonbuild.mesonbuild/changelog"; @@ -4458,8 +4458,8 @@ let mktplcRef = { name = "vscode-stylelint"; publisher = "stylelint"; - version = "2.0.2"; - hash = "sha256-nJYy7HFycKXTQCHgaLP46CGl0hlgaexL1QZ8icGpeVo="; + version = "2.1.0"; + hash = "sha256-cL86Gv2HAtvqNd+2vJPuKAgKVrp5pg6IECFm1Di8Eqk="; }; meta = { description = "Official Stylelint extension for Visual Studio Code"; diff --git a/pkgs/applications/editors/vscode/extensions/eamodio.gitlens/default.nix b/pkgs/applications/editors/vscode/extensions/eamodio.gitlens/default.nix index ed007700d684..695d221ee3f1 100644 --- a/pkgs/applications/editors/vscode/extensions/eamodio.gitlens/default.nix +++ b/pkgs/applications/editors/vscode/extensions/eamodio.gitlens/default.nix @@ -15,13 +15,13 @@ let vsix = stdenv.mkDerivation (finalAttrs: { name = "gitlens-${finalAttrs.version}.vsix"; pname = "gitlens-vsix"; - version = "17.11.0"; + version = "17.11.1"; src = fetchFromGitHub { owner = "gitkraken"; repo = "vscode-gitlens"; tag = "v${finalAttrs.version}"; - hash = "sha256-MMUfl8Vc6mAjs0ZPWV0lHQdqRkKKY0FEx7mbz/yrk9k="; + hash = "sha256-BN6qgPYhZ+FuYnwmV0S3y2vOR4ZLC+VGWuEEPqfOqi4="; }; pnpmDeps = fetchPnpmDeps { diff --git a/pkgs/applications/networking/cluster/terraform-providers/providers.json b/pkgs/applications/networking/cluster/terraform-providers/providers.json index c84be0cd77a6..969eaa262056 100644 --- a/pkgs/applications/networking/cluster/terraform-providers/providers.json +++ b/pkgs/applications/networking/cluster/terraform-providers/providers.json @@ -472,13 +472,13 @@ "vendorHash": "sha256-mzDFyk2oImRXt72kFV5Ln++ScgoecpJEJtzUKjvCaws=" }, "grafana_grafana": { - "hash": "sha256-ifE5W6sUo/BTxO+noss+nqw+LDPlkxdpySlJ08n7Kd4=", + "hash": "sha256-XXnmPZstCrZ2NDMx/azDpvXknuEwqJ+GW0hiaH3+bDQ=", "homepage": "https://registry.terraform.io/providers/grafana/grafana", "owner": "grafana", "repo": "terraform-provider-grafana", - "rev": "v4.27.1", + "rev": "v4.28.1", "spdx": "MPL-2.0", - "vendorHash": "sha256-OCdknvuL/+khhFdopVLKEYKBwLbyPnziKamHDUEX+Zk=" + "vendorHash": "sha256-OMrFGY8rIf32E6/TKSmR/AQPj+GS1e9V5UyPxzhXaNE=" }, "gridscale_gridscale": { "hash": "sha256-FAKvQ/MEod5Ck0PG4ffQ+gQp6zZ0JDRXPOrOiDpWMls=", diff --git a/pkgs/build-support/trivial-builders/default.nix b/pkgs/build-support/trivial-builders/default.nix index 83bbb91374c7..51724c6a331c 100644 --- a/pkgs/build-support/trivial-builders/default.nix +++ b/pkgs/build-support/trivial-builders/default.nix @@ -356,10 +356,16 @@ rec { export ${name} '') runtimeEnv ) - + lib.optionalString (runtimeInputs != [ ]) '' + + '' - export PATH="${lib.makeBinPath runtimeInputs}${lib.optionalString inheritPath ":$PATH"}" + export PATH="${ + lib.concatStringsSep ":" ( + (lib.optionals (runtimeInputs != [ ]) [ (lib.makeBinPath runtimeInputs) ]) + ++ (lib.optionals inheritPath [ "$PATH" ]) + ) + }" '' + + '' ${text} diff --git a/pkgs/build-support/trivial-builders/test/writeShellApplication.nix b/pkgs/build-support/trivial-builders/test/writeShellApplication.nix index 0c15853f91c3..bbf4f5d49da2 100644 --- a/pkgs/build-support/trivial-builders/test/writeShellApplication.nix +++ b/pkgs/build-support/trivial-builders/test/writeShellApplication.nix @@ -83,6 +83,50 @@ linkFarm "writeShellApplication-tests" { ''; }; + test-no-inherit-path-no-runtimeInputs = checkShellApplication { + name = "test-no-inherit-path-no-runtimeInputs"; + inheritPath = false; + runtimeInputs = [ ]; + text = '' + if [[ ''${#PATH} -eq 0 ]]; then + echo -n "PATH is empty" + fi + ''; + expected = "PATH is empty"; + }; + + test-no-inherit-path-runtimeInputs = checkShellApplication { + name = "test-no-inherit-path-runtimeInputs"; + inheritPath = false; + runtimeInputs = [ hello ]; + text = '' + extra_colon_pattern='(^:|:$)' + if [[ ''${PATH} =~ $extra_colon_pattern ]]; then + echo "PATH should not start or end with a colon: $PATH" + fi + if [[ ''${#PATH} -gt 0 ]]; then + echo -n "PATH is not empty" + fi + ''; + expected = "PATH is not empty"; + }; + + test-inherit-path-no-runtimeInputs = checkShellApplication { + name = "test-inherit-path-no-runtimeInputs"; + inheritPath = true; + runtimeInputs = [ ]; + text = '' + extra_colon_pattern='(^:|:$)' + if [[ ''${PATH} =~ $extra_colon_pattern ]]; then + echo "PATH should not start or end with a colon: $PATH" + fi + if [[ ''${#PATH} -gt 0 ]]; then + echo -n "PATH is not empty" + fi + ''; + expected = "PATH is not empty"; + }; + test-check-phase = checkShellApplication { name = "test-check-phase"; text = ""; diff --git a/pkgs/by-name/af/afflib/package.nix b/pkgs/by-name/af/afflib/package.nix index b8076df609dd..58944ead3050 100644 --- a/pkgs/by-name/af/afflib/package.nix +++ b/pkgs/by-name/af/afflib/package.nix @@ -5,7 +5,6 @@ zlib, curl, expat, - fuse, fuse3, openssl, autoreconfHook, @@ -30,9 +29,10 @@ stdenv.mkDerivation (finalAttrs: { expat openssl python3 - ] - ++ lib.optionals stdenv.hostPlatform.isLinux [ fuse3 ] - ++ lib.optionals stdenv.hostPlatform.isDarwin [ fuse ]; + fuse3 + ]; + + env.CFLAGS = lib.optionalString stdenv.hostPlatform.isDarwin "-DFUSE_DARWIN_ENABLE_EXTENSIONS=0"; meta = { homepage = "http://afflib.sourceforge.net/"; diff --git a/pkgs/by-name/au/audio-mirroring/package.nix b/pkgs/by-name/au/audio-mirroring/package.nix index a22460e6d8bc..975a56206512 100644 --- a/pkgs/by-name/au/audio-mirroring/package.nix +++ b/pkgs/by-name/au/audio-mirroring/package.nix @@ -18,18 +18,18 @@ stdenv.mkDerivation rec { pname = "audio-mirroring"; - version = "0.1.1"; + version = "0.1.3"; src = fetchFromGitHub { owner = "mkg20001"; repo = "audio-mirroring"; tag = "v${version}"; - hash = "sha256-f4V5ZJvXhdwqS4kx99Lr2Eb8r08PRd3T4mbRoAyyIqE="; + hash = "sha256-Idu15ZfY8JYVZhub0LRXYtWdiVCMVRyC3MVTX4JcbzY="; }; cargoDeps = rustPlatform.fetchCargoVendor { inherit pname version src; - hash = "sha256-+mAdxaaQOO7AIn/o/J13FbHIvtepk8/okGxO6p6aGzI="; + hash = "sha256-kiDGCl3De5dhDwwCf1F38gnGtfNpAVot0G0+Gxmyyp0="; }; nativeBuildInputs = [ diff --git a/pkgs/by-name/bt/btfs/disable-macfuse-extensions.patch b/pkgs/by-name/bt/btfs/disable-macfuse-extensions.patch new file mode 100644 index 000000000000..624fd3eb9191 --- /dev/null +++ b/pkgs/by-name/bt/btfs/disable-macfuse-extensions.patch @@ -0,0 +1,31 @@ +diff --git a/src/btfs.cc b/src/btfs.cc +index eaac245..6bae7c0 100644 +--- a/src/btfs.cc ++++ b/src/btfs.cc +@@ -19,6 +19,10 @@ along with BTFS. If not, see . + + #define FUSE_USE_VERSION 31 + ++#ifdef __APPLE__ ++#define FUSE_DARWIN_ENABLE_EXTENSIONS 0 ++#endif ++ + #include + #include + #include +@@ -733,15 +737,9 @@ btfs_listxattr(const char *path, char *data, size_t len) { + return xattrslen; + } + +-#ifdef __APPLE__ +-static int +-btfs_getxattr(const char *path, const char *key, char *value, size_t len, +- uint32_t position) { +-#else + static int + btfs_getxattr(const char *path, const char *key, char *value, size_t len) { + uint32_t position = 0; +-#endif + char xattr[16]; + int xattrlen = 0; + diff --git a/pkgs/by-name/bt/btfs/package.nix b/pkgs/by-name/bt/btfs/package.nix index 5f8f48d6aa2c..4a3652a7b999 100644 --- a/pkgs/by-name/bt/btfs/package.nix +++ b/pkgs/by-name/bt/btfs/package.nix @@ -22,6 +22,11 @@ stdenv.mkDerivation (finalAttrs: { sha256 = "sha256-JuofC4TpbZ56qiUrHeoK607YHVbwqwLGMIdUpsTm9Ic="; }; + patches = [ + # https://github.com/johang/btfs/pull/103 + ./disable-macfuse-extensions.patch + ]; + nativeBuildInputs = [ autoreconfHook pkg-config diff --git a/pkgs/by-name/cg/cgl/package.nix b/pkgs/by-name/cg/cgl/package.nix index 5e090e2129c4..40173729709c 100644 --- a/pkgs/by-name/cg/cgl/package.nix +++ b/pkgs/by-name/cg/cgl/package.nix @@ -10,13 +10,13 @@ stdenv.mkDerivation (finalAttrs: { pname = "cgl"; - version = "0.60.9"; + version = "0.60.10"; src = fetchFromGitHub { owner = "coin-or"; repo = "Cgl"; rev = "releases/${finalAttrs.version}"; - hash = "sha256-E84yCrgpRMjt7owPLPk1ATW+aeHNw8V24DHgkb6boIE="; + hash = "sha256-zkq8pdn4m56sGd3I6xID3M+u7BxVp0S5naKBjqAdeyE="; }; nativeBuildInputs = [ diff --git a/pkgs/by-name/cn/cnspec/package.nix b/pkgs/by-name/cn/cnspec/package.nix index dc0ec7e52827..da4ace22b774 100644 --- a/pkgs/by-name/cn/cnspec/package.nix +++ b/pkgs/by-name/cn/cnspec/package.nix @@ -6,18 +6,18 @@ buildGoModule (finalAttrs: { pname = "cnspec"; - version = "13.0.0"; + version = "13.1.1"; src = fetchFromGitHub { owner = "mondoohq"; repo = "cnspec"; tag = "v${finalAttrs.version}"; - hash = "sha256-qA48TBt1S4M6xyvfBELxbJd0R7PwY34naZctb4XRnwo="; + hash = "sha256-579zSogioTKdsqOwTptJUqN1IEWnPzEmWrSjllqIYOY="; }; proxyVendor = true; - vendorHash = "sha256-CwR0/L+ptBKjBLLZ7I96+jxJyCAgM7V0etXz+H0vlhI="; + vendorHash = "sha256-ZPJGtI5HTetjSDfkXmF2elyXPO7AmQn1zmXzEjNIIXc="; subPackages = [ "apps/cnspec" ]; diff --git a/pkgs/by-name/co/codebook/package.nix b/pkgs/by-name/co/codebook/package.nix index 53ca36de1b89..2c4504e8d42f 100644 --- a/pkgs/by-name/co/codebook/package.nix +++ b/pkgs/by-name/co/codebook/package.nix @@ -8,17 +8,17 @@ rustPlatform.buildRustPackage (finalAttrs: { pname = "codebook"; - version = "0.3.32"; + version = "0.3.34"; src = fetchFromGitHub { owner = "blopker"; repo = "codebook"; tag = "v${finalAttrs.version}"; - hash = "sha256-UkE1ND1ditGIlplHG6EslK2uDvRWz7jmn2UmUhlYbdE="; + hash = "sha256-BMPwYw7BHywyDJLgHzJt6HsrI23Y+Ng+vcUdFNJH68M="; }; buildAndTestSubdir = "crates/codebook-lsp"; - cargoHash = "sha256-27+9vjTHBxJ3WM2e3xmTO2CmJvsmqN4nhqD0Sf0YtEw="; + cargoHash = "sha256-q6oEHXGxItR9GW2vqpj2i6AN0hH8ybMQ+vkX4aljt/I="; env = { CARGO_PROFILE_RELEASE_LTO = "fat"; diff --git a/pkgs/by-name/di/dislocker/package.nix b/pkgs/by-name/di/dislocker/package.nix index 5b69c53d45ae..cb6450ee6a2d 100644 --- a/pkgs/by-name/di/dislocker/package.nix +++ b/pkgs/by-name/di/dislocker/package.nix @@ -29,6 +29,8 @@ stdenv.mkDerivation (finalAttrs: { mbedtls ]; + env.CFLAGS = lib.optionalString stdenv.hostPlatform.isDarwin "-DFUSE_DARWIN_ENABLE_EXTENSIONS=0"; + meta = { description = "Read BitLocker encrypted partitions in Linux"; homepage = "https://github.com/Aorimn/dislocker"; diff --git a/pkgs/by-name/ff/fflogs/package.nix b/pkgs/by-name/ff/fflogs/package.nix index bb60a8d1df79..8fd44c0dcd99 100644 --- a/pkgs/by-name/ff/fflogs/package.nix +++ b/pkgs/by-name/ff/fflogs/package.nix @@ -6,10 +6,10 @@ let pname = "fflogs"; - version = "8.20.113"; + version = "9.0.24"; src = fetchurl { url = "https://github.com/RPGLogs/Uploaders-fflogs/releases/download/v${version}/fflogs-v${version}.AppImage"; - hash = "sha256-mwbATBhkbeZ2f4KAytOgp8XbCL4dY7S7OPHj//4kqGQ="; + hash = "sha256-9L9eNpK2MI3P+mhUDCAzfi3YDdWpHGjiUS5LjksUjqo="; }; extracted = appimageTools.extractType2 { inherit pname version src; }; in diff --git a/pkgs/by-name/fo/fosrl-olm/package.nix b/pkgs/by-name/fo/fosrl-olm/package.nix index 9539ee95805d..2e7e8467ed27 100644 --- a/pkgs/by-name/fo/fosrl-olm/package.nix +++ b/pkgs/by-name/fo/fosrl-olm/package.nix @@ -6,16 +6,16 @@ buildGoModule (finalAttrs: { pname = "olm"; - version = "1.4.2"; + version = "1.4.3"; src = fetchFromGitHub { owner = "fosrl"; repo = "olm"; tag = finalAttrs.version; - hash = "sha256-Tily8Srpr5GpKTYl3Ivm1b/VN2yEzbbHHABeoJvo3wo="; + hash = "sha256-4dzbSW9AoFitypVOD/N4/mnUJwh0USgOwVqcopLkcYs="; }; - vendorHash = "sha256-lqH/pMWeDsTJa39uJwHntCAUs0BwJiB0aMyFaI++5ms="; + vendorHash = "sha256-D93SPwXAeoTLCbScjyH8AB9TJIF2b/UbLNMIQYi+B+c="; ldflags = [ "-s" diff --git a/pkgs/by-name/ft/ft2-clone/package.nix b/pkgs/by-name/ft/ft2-clone/package.nix index 1459c43cd5b5..0a67b87541e6 100644 --- a/pkgs/by-name/ft/ft2-clone/package.nix +++ b/pkgs/by-name/ft/ft2-clone/package.nix @@ -11,13 +11,13 @@ stdenv.mkDerivation (finalAttrs: { pname = "ft2-clone"; - version = "2.11"; + version = "2.12"; src = fetchFromGitHub { owner = "8bitbubsy"; repo = "ft2-clone"; rev = "v${finalAttrs.version}"; - hash = "sha256-thOQcsnFkDJh0P2Yu/1rCmt/M3Ikr88ffFHUDrgFNyk="; + hash = "sha256-Ca4vp2uEF7rZJ+0lAmVqC/6F+2CgbDLK2GkbG5Tn//0="; }; nativeBuildInputs = [ cmake ]; diff --git a/pkgs/by-name/gi/gitlab-runner/package.nix b/pkgs/by-name/gi/gitlab-runner/package.nix index 185e8eca8dc1..8866e5a57ed5 100644 --- a/pkgs/by-name/gi/gitlab-runner/package.nix +++ b/pkgs/by-name/gi/gitlab-runner/package.nix @@ -6,20 +6,21 @@ fetchFromGitLab, nix-update-script, versionCheckHook, + writableTmpDirAsHomeHook, }: buildGoModule (finalAttrs: { pname = "gitlab-runner"; - version = "18.8.0"; + version = "18.9.0"; src = fetchFromGitLab { owner = "gitlab-org"; repo = "gitlab-runner"; tag = "v${finalAttrs.version}"; - hash = "sha256-rS7+BUdec+Z4G/dd5D/NHe3gbELWicg0Nmgx4zJAIX4="; + hash = "sha256-U13SouwEfCVy5M8fv6rkCX0F+ecVYdsocvAdt3yxPJA="; }; - vendorHash = "sha256-Br9TW+sg7PDOE2d8lVQ9Xv9+UD7JHzitdTOcyodHr+s="; + vendorHash = "sha256-Ak1Q8FnTD8LKcN9xRc1gpcnUiambGC3CJP84cwQqTtM="; # For patchShebangs buildInputs = [ bash ]; @@ -85,6 +86,8 @@ buildGoModule (finalAttrs: { "-X ${ldflagsPackageVariablePrefix}.REVISION=v${finalAttrs.version}" ]; + nativeCheckInputs = [ writableTmpDirAsHomeHook ]; + preCheck = '' # Make the tests pass outside of GitLab CI export CI=0 diff --git a/pkgs/by-name/gi/gitlab-runner/remove-bash-test.patch b/pkgs/by-name/gi/gitlab-runner/remove-bash-test.patch index 6f27ef71c957..0365002efec9 100644 --- a/pkgs/by-name/gi/gitlab-runner/remove-bash-test.patch +++ b/pkgs/by-name/gi/gitlab-runner/remove-bash-test.patch @@ -1,5 +1,5 @@ diff --git a/shells/bash_test.go b/shells/bash_test.go -index 9ed9e65ff..02b6e6d5f 100644 +index bbbe949f4..955992d3f 100644 --- a/shells/bash_test.go +++ b/shells/bash_test.go @@ -4,11 +4,9 @@ package shells @@ -11,10 +11,10 @@ index 9ed9e65ff..02b6e6d5f 100644 "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "gitlab.com/gitlab-org/gitlab-runner/common" - ) -@@ -90,65 +88,6 @@ func TestBash_CheckForErrors(t *testing.T) { + "gitlab.com/gitlab-org/gitlab-runner/common" + "gitlab.com/gitlab-org/gitlab-runner/common/spec" +@@ -78,65 +76,6 @@ func TestBash_CheckForErrors(t *testing.T) { } } diff --git a/pkgs/by-name/gl/globulation2/package.nix b/pkgs/by-name/gl/globulation2/package.nix index 9f4035bd64f2..b1a210712b32 100644 --- a/pkgs/by-name/gl/globulation2/package.nix +++ b/pkgs/by-name/gl/globulation2/package.nix @@ -43,19 +43,15 @@ stdenv.mkDerivation rec { sha256 = "0017xg5agj3dy0hx71ijdcrxb72bjqv7x6aq7c9zxzyyw0mkxj0k"; }) (fetchpatch { - url = "https://sources.debian.org/data/main/g/glob2/0.9.4.4-6/debian/patches/10_pthread_underlinkage.patch"; + url = "https://sources.debian.org/data/main/g/glob2/0.9.4.4-11/debian/patches/10_pthread_underlinkage.patch"; sha256 = "sha256-L9POADlkgQbUQEUmx4s3dxXG9tS0w2IefpRGuQNRMI0="; }) (fetchpatch { - url = "https://sources.debian.org/data/main/g/glob2/0.9.4.4-6/debian/patches/link-boost-system.patch"; - sha256 = "sha256-ne6F2ZowB+TUmg3ePuUoPNxXI0ZJC6HEol3oQQHJTy4="; + url = "https://sources.debian.org/data/main/g/glob2/0.9.4.4-11/debian/patches/scons.patch"; + sha256 = "sha256-kHuFQCmkCkogqK6vfHKGYeZrMvsdQ7h8B3CcCtjLr50="; }) (fetchpatch { - url = "https://sources.debian.org/data/main/g/glob2/0.9.4.4-6/debian/patches/scons.patch"; - sha256 = "sha256-Gah7SoVcd/Aljs0Nqo3YF0lZImUWtrGM4HbbQ4yrhHU="; - }) - (fetchpatch { - url = "https://sources.debian.org/data/main/g/glob2/0.9.4.4-6/debian/patches/boost-1.69.patch"; + url = "https://sources.debian.org/data/main/g/glob2/0.9.4.4-11/debian/patches/boost-1.69.patch"; sha256 = "sha256-D7agFR4uyIHxQz690Q8EHPF+rTEoiGUpgkm7r5cL5SI="; }) ]; @@ -77,6 +73,7 @@ stdenv.mkDerivation rec { scons bsdiff # bspatch ]; + buildInputs = [ libGLU libGL @@ -98,8 +95,6 @@ stdenv.mkDerivation rec { "DATADIR=${placeholder "out"}/share/globulation2/glob2" ]; - env.NIX_LDFLAGS = "-lboost_system"; - meta = { description = "RTS without micromanagement"; mainProgram = "glob2"; diff --git a/pkgs/by-name/go/goupile/package.nix b/pkgs/by-name/go/goupile/package.nix new file mode 100644 index 000000000000..a99a5f88539e --- /dev/null +++ b/pkgs/by-name/go/goupile/package.nix @@ -0,0 +1,77 @@ +{ + lib, + fetchFromGitHub, + versionCheckHook, + installShellFiles, + + stdenv, + clangStdenv, + llvmPackages, + nixosTests, + + # https://goupile.org/en/build recommends a Paranoid build + # which is not bit by bit reproducible, whereas others are + profile ? "Paranoid", +}: + +assert lib.assertOneOf "profile" profile [ + "Fast" + "Debug" + "Paranoid" +]; + +let + stdenv' = if (profile == "Paranoid") then clangStdenv else stdenv; +in +stdenv'.mkDerivation (finalAttrs: { + pname = "goupile"; + version = "3.12.1"; + + # https://github.com/Koromix/rygel/tags + src = fetchFromGitHub { + owner = "Koromix"; + repo = "rygel"; + tag = "goupile/${finalAttrs.version}"; + hash = "sha256-Pn/0tjezVKJedAtqj69avxeIK2l3l9FGioYSyEao12E="; + }; + + nativeBuildInputs = [ + installShellFiles + ] + ++ lib.optionals (profile == "Paranoid") [ + llvmPackages.bintools + ]; + + # pipe2() is only exposed with _GNU_SOURCE + NIX_CFLAGS_COMPILE = [ "-D_GNU_SOURCE" ]; + + buildPhase = '' + runHook preBuild + ./bootstrap.sh + echo "goupile = ${finalAttrs.version}" >FelixVersions.ini + ./felix -s -p${profile} goupile + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + installBin bin/${profile}/goupile + runHook postInstall + ''; + + doInstallCheck = true; + versionCheckProgramArg = "--version"; + nativeInstallCheckInputs = [ versionCheckHook ]; + + passthru.tests = { inherit (nixosTests) goupile; }; + + meta = { + changelog = "https://github.com/Koromix/rygel/blob/${finalAttrs.src.rev}/src/goupile/CHANGELOG.md"; + description = "Free design tool for secure forms including Clinical Report Forms (eCRF)"; + homepage = "https://goupile.org/en"; + license = lib.licenses.gpl3Plus; # sdpx headers + platforms = lib.platforms.linux; # https://goupile.org/en/build + mainProgram = "goupile"; + teams = with lib.teams; [ ngi ]; + }; +}) diff --git a/pkgs/by-name/gr/grafana-alloy/package.nix b/pkgs/by-name/gr/grafana-alloy/package.nix index 208ac69db7d6..cc6fcf59da32 100644 --- a/pkgs/by-name/gr/grafana-alloy/package.nix +++ b/pkgs/by-name/gr/grafana-alloy/package.nix @@ -6,105 +6,106 @@ buildGoModule, buildNpmPackage, systemd, - grafana-alloy, + installShellFiles, + versionCheckHook, nixosTests, nix-update-script, - installShellFiles, - testers, lld, useLLD ? stdenv.hostPlatform.isArmv7, }: - buildGoModule (finalAttrs: { pname = "grafana-alloy"; - version = "1.12.2"; + version = "1.14.1"; + src = fetchFromGitHub { owner = "grafana"; repo = "alloy"; tag = "v${finalAttrs.version}"; - hash = "sha256-C/yqsUjEwKnGRkxMOQkKfGdeERbvO/e7D7c3CyJ+cVY="; + hash = "sha256-zgbbbuq+sb+nU1vgzaxEHGY77k+TXFrlvcvs/NSqQAM="; }; npmDeps = fetchNpmDeps { src = "${finalAttrs.src}/internal/web/ui"; - hash = "sha256-3J1Slka5bi+72NUaHBmDTtG1faJWRkOlkClKnUyiUsk="; + hash = "sha256-GT0yisPn+3FCtWL3he0i5zPMlaWNparQDefU69G4Yis="; }; frontend = buildNpmPackage { pname = "alloy-frontend"; inherit (finalAttrs) version src; - inherit (finalAttrs) npmDeps; sourceRoot = "${finalAttrs.src.name}/internal/web/ui"; + inherit (finalAttrs) npmDeps; + installPhase = '' runHook preInstall - mkdir $out + mkdir -p $out cp -av dist $out/share runHook postInstall ''; }; - proxyVendor = true; - vendorHash = "sha256-Bq/6ld2LldSDhksNqGMHXZAeNHh74D07o2ETpQqMcP4="; + patchPhase = '' + cp -av ${finalAttrs.frontend}/share internal/web/ui/dist + ''; - nativeBuildInputs = [ - installShellFiles + modRoot = "collector"; + + proxyVendor = true; + vendorHash = "sha256-A1mbMmpUxg5T7//X5PL1CPGB1OMPhertFvz4sPFTgOg="; + + subPackages = [ "." ]; + + ldflags = [ + "-s" + "-w" + "-X github.com/grafana/alloy/internal/build.Version=${finalAttrs.version}" + "-X github.com/grafana/alloy/internal/build.Branch=v${finalAttrs.version}" + "-X github.com/grafana/alloy/internal/build.Revision=v${finalAttrs.version}" + "-X github.com/grafana/alloy/internal/build.BuildUser=nix@nixpkgs" + "-X github.com/grafana/alloy/internal/build.BuildDate=1970-01-01T00:00:00Z" + ]; + + tags = [ + "embedalloyui" + "netgo" ] - ++ lib.optionals useLLD [ lld ]; + ++ lib.optionals stdenv.hostPlatform.isLinux [ + "promtail_journal_enabled" + ]; env = lib.optionalAttrs useLLD { NIX_CFLAGS_LINK = "-fuse-ld=lld"; } // lib.optionalAttrs (stdenv.hostPlatform.isLinux) { - # uses go-systemd, which uses libsystemd headers + # Uses go-systemd, which uses libsystemd headers. # https://github.com/coreos/go-systemd/issues/351 NIX_CFLAGS_COMPILE = "-I${lib.getDev systemd}/include"; }; - ldflags = - let - prefix = "github.com/grafana/alloy/internal/build"; - in - [ - "-s" - "-w" - # https://github.com/grafana/alloy/blob/3201389252d2c011bee15ace0c9f4cdbcb978f9f/Makefile#L110 - "-X ${prefix}.Branch=v${finalAttrs.version}" - "-X ${prefix}.Version=${finalAttrs.version}" - "-X ${prefix}.Revision=v${finalAttrs.version}" - "-X ${prefix}.BuildUser=nix" - "-X ${prefix}.BuildDate=1970-01-01T00:00:00Z" - ]; + nativeBuildInputs = [ + installShellFiles + ] + ++ lib.optionals useLLD [ lld ]; - tags = [ - "netgo" - "builtinassets" - "promtail_journal_enabled" - ]; + postInstall = lib.optionalString (stdenv.buildPlatform.canExecute stdenv.hostPlatform) '' + mv -v $out/bin/otel_engine $out/bin/alloy - patchPhase = '' - # Copy frontend build in - cp -va "${finalAttrs.frontend}/share" "internal/web/ui/dist" + installShellCompletion --cmd alloy \ + --bash <($out/bin/alloy completion bash) \ + --fish <($out/bin/alloy completion fish) \ + --zsh <($out/bin/alloy completion zsh) ''; - subPackages = [ - "." - ]; - - checkFlags = [ - "-tags" - "nonetwork" # disable network tests - "-tags" - "nodocker" # disable docker tests - ]; + doInstallCheck = true; + nativeInstallCheckInputs = [ versionCheckHook ]; + versionCheckProgramArg = "-v"; # go-systemd uses libsystemd under the hood, which does dlopen(libsystemd) at - # runtime. - # Add to RUNPATH so it can be found. + # runtime. Add to RPATH so it can be found. postFixup = lib.optionalString stdenv.hostPlatform.isLinux '' patchelf \ --set-rpath "${ @@ -113,20 +114,9 @@ buildGoModule (finalAttrs: { $out/bin/alloy ''; - postInstall = lib.optionalString (stdenv.buildPlatform.canExecute stdenv.hostPlatform) '' - installShellCompletion --cmd alloy \ - --bash <($out/bin/alloy completion bash) \ - --fish <($out/bin/alloy completion fish) \ - --zsh <($out/bin/alloy completion zsh) - ''; - passthru = { tests = { inherit (nixosTests) alloy; - version = testers.testVersion { - version = "v${finalAttrs.version}"; - package = grafana-alloy; - }; }; updateScript = nix-update-script { extraArgs = [ @@ -134,21 +124,26 @@ buildGoModule (finalAttrs: { "v(.+)" ]; }; - # for nix-update to be able to find and update the hash + # For nix-update to be able to find and update the hash. inherit (finalAttrs) npmDeps; }; meta = { - description = "Open source OpenTelemetry Collector distribution with built-in Prometheus pipelines and support for metrics, logs, traces, and profiles"; - mainProgram = "alloy"; - license = lib.licenses.asl20; + description = "OpenTelemetry Collector distribution with programmable pipelines"; + longDescription = '' + Grafana Alloy is an open source OpenTelemetry Collector distribution with + built-in Prometheus pipelines and support for metrics, logs, traces, and + profiles. + ''; homepage = "https://grafana.com/oss/alloy"; changelog = "https://github.com/grafana/alloy/blob/${finalAttrs.src.rev}/CHANGELOG.md"; + license = lib.licenses.asl20; + platforms = lib.platforms.unix; maintainers = with lib.maintainers; [ azahi flokli hbjydev ]; - platforms = lib.platforms.unix; + mainProgram = "alloy"; }; }) diff --git a/pkgs/by-name/ju/just/package.nix b/pkgs/by-name/ju/just/package.nix index 9be4d7dfc50a..f34216b76d66 100644 --- a/pkgs/by-name/ju/just/package.nix +++ b/pkgs/by-name/ju/just/package.nix @@ -17,7 +17,7 @@ withDocumentation ? stdenv.buildPlatform.canExecute stdenv.hostPlatform, }: let - version = "1.46.0"; + version = "1.47.1"; in rustPlatform.buildRustPackage { inherit version; @@ -34,10 +34,10 @@ rustPlatform.buildRustPackage { owner = "casey"; repo = "just"; tag = version; - hash = "sha256-NE54LKS2bYBfQL+yLJPaG4iF7EiJfDqBfnsrlPo1+OE="; + hash = "sha256-HGrUiPe4vVYNISovTb9PZt8s6xCUg+OWkrp8dPm9tWg="; }; - cargoHash = "sha256-yyaJAWp6luizA/aQuUGhdxRX2Ofri4CeLIO3/ndSCzc="; + cargoHash = "sha256-ZRcYVvodaQmQtBGnkTIOI3PXC6YQ1kqycm6Xh/MwIqA="; nativeBuildInputs = lib.optionals (installShellCompletions || installManPages) [ installShellFiles ] diff --git a/pkgs/by-name/ke/keycloak/package.nix b/pkgs/by-name/ke/keycloak/package.nix index 19965cf9a5d2..d4a926b6c190 100644 --- a/pkgs/by-name/ke/keycloak/package.nix +++ b/pkgs/by-name/ke/keycloak/package.nix @@ -24,11 +24,11 @@ let in stdenv.mkDerivation (finalAttrs: { pname = "keycloak"; - version = "26.5.5"; + version = "26.5.6"; src = fetchzip { url = "https://github.com/keycloak/keycloak/releases/download/${finalAttrs.version}/keycloak-${finalAttrs.version}.zip"; - hash = "sha256-k6keuENMQ1S+4YN67E6vc48W8x4Le0Bw9E1+UBLyxh0="; + hash = "sha256-lkBSzM0kPYe3301EJkY/NShaKpBz+7NuAK/MPNLwMX4="; }; nativeBuildInputs = [ diff --git a/pkgs/by-name/ki/kicad/package.nix b/pkgs/by-name/ki/kicad/package.nix index 60218a56b109..f9dd887a0401 100644 --- a/pkgs/by-name/ki/kicad/package.nix +++ b/pkgs/by-name/ki/kicad/package.nix @@ -332,7 +332,10 @@ stdenv.mkDerivation rec { The Programs handle Schematic Capture, and PCB Layout with Gerber output. ''; license = lib.licenses.gpl3Plus; - maintainers = with lib.maintainers; [ korken89 ]; + maintainers = with lib.maintainers; [ + korken89 + ryand56 + ]; platforms = lib.platforms.all; broken = stdenv.hostPlatform.isDarwin; mainProgram = "kicad"; diff --git a/pkgs/by-name/la/laravel/composer.lock b/pkgs/by-name/la/laravel/composer.lock index 506f0e116d14..503375012f41 100644 --- a/pkgs/by-name/la/laravel/composer.lock +++ b/pkgs/by-name/la/laravel/composer.lock @@ -167,16 +167,16 @@ }, { "name": "illuminate/collections", - "version": "v12.53.0", + "version": "v12.54.1", "source": { "type": "git", "url": "https://github.com/illuminate/collections.git", - "reference": "f35c084f0d9bc57895515cb4d0665797c66285fd" + "reference": "86f874536cbda5f35c23a9908ee7f176caa4496e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/collections/zipball/f35c084f0d9bc57895515cb4d0665797c66285fd", - "reference": "f35c084f0d9bc57895515cb4d0665797c66285fd", + "url": "https://api.github.com/repos/illuminate/collections/zipball/86f874536cbda5f35c23a9908ee7f176caa4496e", + "reference": "86f874536cbda5f35c23a9908ee7f176caa4496e", "shasum": "" }, "require": { @@ -223,11 +223,11 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2026-02-16T14:10:38+00:00" + "time": "2026-02-25T15:25:18+00:00" }, { "name": "illuminate/conditionable", - "version": "v12.53.0", + "version": "v12.54.1", "source": { "type": "git", "url": "https://github.com/illuminate/conditionable.git", @@ -273,7 +273,7 @@ }, { "name": "illuminate/contracts", - "version": "v12.53.0", + "version": "v12.54.1", "source": { "type": "git", "url": "https://github.com/illuminate/contracts.git", @@ -321,16 +321,16 @@ }, { "name": "illuminate/filesystem", - "version": "v12.53.0", + "version": "v12.54.1", "source": { "type": "git", "url": "https://github.com/illuminate/filesystem.git", - "reference": "c4c3f8612f218afcf09f3c7f5c7dc9e282626800" + "reference": "b91eede30e1bde98cb51fb4c4f28269a8dea593e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/filesystem/zipball/c4c3f8612f218afcf09f3c7f5c7dc9e282626800", - "reference": "c4c3f8612f218afcf09f3c7f5c7dc9e282626800", + "url": "https://api.github.com/repos/illuminate/filesystem/zipball/b91eede30e1bde98cb51fb4c4f28269a8dea593e", + "reference": "b91eede30e1bde98cb51fb4c4f28269a8dea593e", "shasum": "" }, "require": { @@ -384,11 +384,11 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2026-02-13T20:26:32+00:00" + "time": "2026-03-09T14:26:54+00:00" }, { "name": "illuminate/macroable", - "version": "v12.53.0", + "version": "v12.54.1", "source": { "type": "git", "url": "https://github.com/illuminate/macroable.git", @@ -434,16 +434,16 @@ }, { "name": "illuminate/reflection", - "version": "v12.53.0", + "version": "v12.54.1", "source": { "type": "git", "url": "https://github.com/illuminate/reflection.git", - "reference": "6188e97a587371b9951c2a7e337cd760308c17d7" + "reference": "348cf5da9de89b596d7723be6425fb048e2bf4bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/reflection/zipball/6188e97a587371b9951c2a7e337cd760308c17d7", - "reference": "6188e97a587371b9951c2a7e337cd760308c17d7", + "url": "https://api.github.com/repos/illuminate/reflection/zipball/348cf5da9de89b596d7723be6425fb048e2bf4bb", + "reference": "348cf5da9de89b596d7723be6425fb048e2bf4bb", "shasum": "" }, "require": { @@ -481,20 +481,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2026-02-04T15:21:22+00:00" + "time": "2026-02-25T15:25:18+00:00" }, { "name": "illuminate/support", - "version": "v12.53.0", + "version": "v12.54.1", "source": { "type": "git", "url": "https://github.com/illuminate/support.git", - "reference": "18d7d75366ddb9eded3b7f05173f791da47faf34" + "reference": "e54208c0b5693becd8d3bec02f07e8db9aa4f512" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/support/zipball/18d7d75366ddb9eded3b7f05173f791da47faf34", - "reference": "18d7d75366ddb9eded3b7f05173f791da47faf34", + "url": "https://api.github.com/repos/illuminate/support/zipball/e54208c0b5693becd8d3bec02f07e8db9aa4f512", + "reference": "e54208c0b5693becd8d3bec02f07e8db9aa4f512", "shasum": "" }, "require": { @@ -561,20 +561,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2026-02-23T15:44:06+00:00" + "time": "2026-03-06T15:24:01+00:00" }, { "name": "laravel/prompts", - "version": "v0.3.13", + "version": "v0.3.14", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "ed8c466571b37e977532fb2fd3c272c784d7050d" + "reference": "9f0e371244eedfe2ebeaa72c79c54bb5df6e0176" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/ed8c466571b37e977532fb2fd3c272c784d7050d", - "reference": "ed8c466571b37e977532fb2fd3c272c784d7050d", + "url": "https://api.github.com/repos/laravel/prompts/zipball/9f0e371244eedfe2ebeaa72c79c54bb5df6e0176", + "reference": "9f0e371244eedfe2ebeaa72c79c54bb5df6e0176", "shasum": "" }, "require": { @@ -618,22 +618,22 @@ "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.3.13" + "source": "https://github.com/laravel/prompts/tree/v0.3.14" }, - "time": "2026-02-06T12:17:10+00:00" + "time": "2026-03-01T09:02:38+00:00" }, { "name": "nesbot/carbon", - "version": "3.11.1", + "version": "3.11.3", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "f438fcc98f92babee98381d399c65336f3a3827f" + "reference": "6a7e652845bb018c668220c2a545aded8594fbbf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/f438fcc98f92babee98381d399c65336f3a3827f", - "reference": "f438fcc98f92babee98381d399c65336f3a3827f", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/6a7e652845bb018c668220c2a545aded8594fbbf", + "reference": "6a7e652845bb018c668220c2a545aded8594fbbf", "shasum": "" }, "require": { @@ -725,7 +725,7 @@ "type": "tidelift" } ], - "time": "2026-01-29T09:26:29+00:00" + "time": "2026-03-11T17:23:39+00:00" }, { "name": "psr/clock", @@ -958,16 +958,16 @@ }, { "name": "symfony/console", - "version": "v7.4.6", + "version": "v7.4.7", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "6d643a93b47398599124022eb24d97c153c12f27" + "reference": "e1e6770440fb9c9b0cf725f81d1361ad1835329d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/6d643a93b47398599124022eb24d97c153c12f27", - "reference": "6d643a93b47398599124022eb24d97c153c12f27", + "url": "https://api.github.com/repos/symfony/console/zipball/e1e6770440fb9c9b0cf725f81d1361ad1835329d", + "reference": "e1e6770440fb9c9b0cf725f81d1361ad1835329d", "shasum": "" }, "require": { @@ -1032,7 +1032,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.6" + "source": "https://github.com/symfony/console/tree/v7.4.7" }, "funding": [ { @@ -1052,7 +1052,7 @@ "type": "tidelift" } ], - "time": "2026-02-25T17:02:47+00:00" + "time": "2026-03-06T14:06:20+00:00" }, { "name": "symfony/deprecation-contracts", @@ -2495,11 +2495,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.40", + "version": "2.1.41", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b", - "reference": "9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/a2eae8f20856b3afe74bf1f9726ce8c11438e300", + "reference": "a2eae8f20856b3afe74bf1f9726ce8c11438e300", "shasum": "" }, "require": { @@ -2544,7 +2544,7 @@ "type": "github" } ], - "time": "2026-02-23T15:04:35+00:00" + "time": "2026-03-16T18:24:10+00:00" }, { "name": "phpunit/php-code-coverage", diff --git a/pkgs/by-name/la/laravel/package.nix b/pkgs/by-name/la/laravel/package.nix index b661092e5260..64335a187fef 100644 --- a/pkgs/by-name/la/laravel/package.nix +++ b/pkgs/by-name/la/laravel/package.nix @@ -7,19 +7,19 @@ }: php.buildComposerProject2 (finalAttrs: { pname = "laravel"; - version = "5.24.7"; + version = "5.24.9"; src = fetchFromGitHub { owner = "laravel"; repo = "installer"; tag = "v${finalAttrs.version}"; - hash = "sha256-szyoqX4wgJpQZO9H/WVq70A5n/3qV1SdBCKQc9vm4WY="; + hash = "sha256-RlY6is5rRks2mXdE2/EXuSWX2CxJuK+q8yfsDcZMFBo="; }; nativeBuildInputs = [ makeWrapper ]; composerLock = ./composer.lock; - vendorHash = "sha256-yX6EmbopVUpbbVBfep1Rk84wUK5sxjrlzvii+s39SqA="; + vendorHash = "sha256-o7YryCZjTm/O4ts21NjODqacdXnjWZUH8Dmr8fPnDEg="; # Adding npm (nodejs) and php composer to path postInstall = '' diff --git a/pkgs/by-name/mu/multiviewer-for-f1/package.nix b/pkgs/by-name/mu/multiviewer-for-f1/package.nix index 2b193ae0bc27..22ec0cc022a8 100644 --- a/pkgs/by-name/mu/multiviewer-for-f1/package.nix +++ b/pkgs/by-name/mu/multiviewer-for-f1/package.nix @@ -31,15 +31,15 @@ writeScript, }: let - id = "354596705"; + id = "373278730"; in stdenvNoCC.mkDerivation (finalAttrs: { pname = "multiviewer-for-f1"; - version = "2.5.1"; + version = "2.7.1"; src = fetchurl { - url = "https://releases.multiviewer.dev/download/${id}/multiviewer_${finalAttrs.version}_amd64.deb"; - hash = "sha256-9ts5CZD14CzJHiC3YoKWIEKiFpOrcUX1tRUhE4it5Mo="; + url = "https://releases.multiviewer.app/download/${id}/multiviewer_${finalAttrs.version}_amd64.deb"; + hash = "sha256-BKXw8a4fUT+B7KBc6p/Heo+sAtWAG5b/D2iohuNOotY="; }; nativeBuildInputs = [ diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py index aa97b952519a..6699cc1778e1 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py @@ -25,14 +25,12 @@ from .models import ( Profile, Remote, ) -from .process import PRESERVE_ENV, SSH_DEFAULT_OPTS, run_wrapper, run_wrapper_bg -from .process import Args as ProcessArgs +from .process import PRESERVE_ENV, SSH_DEFAULT_OPTS, run_wrapper from .utils import Args, dict_to_flags FLAKE_FLAGS: Final = ["--extra-experimental-features", "nix-command flakes"] FLAKE_REPL_TEMPLATE: Final = "repl.nix.template" -SYSTEMD_RUN_UNIT_PREFIX: Final = "nixos-rebuild-switch-to-configuration" -SYSTEMD_RUN_CMD_PREFIX: Final = [ +SWITCH_TO_CONFIGURATION_CMD_PREFIX: Final = [ "systemd-run", "-E", # Will be set to new value early in switch-to-configuration script, @@ -43,10 +41,11 @@ SYSTEMD_RUN_CMD_PREFIX: Final = [ "-E", "NIXOS_NO_CHECK", "--collect", - "--wait", "--no-ask-password", + "--pipe", "--quiet", "--service-type=exec", + "--unit=nixos-rebuild-switch-to-configuration", ] logger: Final = logging.getLogger(__name__) @@ -207,17 +206,20 @@ def copy_closure( append_local_env=env, ) - def nix_copy(to_host: Remote, from_host: Remote) -> None: + def nix_copy(to_host: Remote | None, from_host: Remote | None) -> None: + host_flags = [] + if from_host is not None: + host_flags += ["--from", f"{from_host.store_type}://{from_host.host}"] + if to_host is not None: + host_flags += ["--to", f"{to_host.store_type}://{to_host.host}"] + run_wrapper( [ "nix", *FLAKE_FLAGS, "copy", *dict_to_flags(copy_flags), - "--from", - f"ssh://{from_host.host}", - "--to", - f"ssh://{to_host.host}", + *host_flags, closure, ], append_local_env=env, @@ -226,9 +228,12 @@ def copy_closure( match (to_host, from_host): case (x, y) if x == y: return - case (Remote(_) as host, None) | (None, Remote(_) as host): + # nix-copy-closure doesn't support store types other than "ssh". + case (Remote(_) as host, None) | (None, Remote(_) as host) if ( + host.store_type == "ssh" + ): nix_copy_closure(host, to=bool(to_host)) - case (Remote(_), Remote(_)): + case (Remote(_), _) | (_, Remote(_)): nix_copy(to_host, from_host) @@ -437,7 +442,10 @@ def get_generations(profile: Profile) -> list[Generation]: ) return sorted( - [parse_path(p, profile) for p in profile.path.parent.glob("system-*-link")], + [ + parse_path(p, profile) + for p in profile.path.parent.glob(f"{profile.name}-*-link") + ], key=lambda d: d.id, ) @@ -662,80 +670,6 @@ def set_profile( remote=target_host, sudo=sudo, ) - - -def _has_systemd(target_host: Remote | None) -> bool: - r = run_wrapper( - ["test", "-d", "/run/systemd/system"], - remote=target_host, - check=False, - ) - return r.returncode == 0 - - -def _run_action( - action: Literal[Action.SWITCH, Action.BOOT, Action.TEST, Action.DRY_ACTIVATE], - path_to_config: Path, - install_bootloader: bool, - target_host: Remote | None, - sudo: bool, - prefix: ProcessArgs | None = None, -) -> None: - cmd: ProcessArgs = [path_to_config / "bin/switch-to-configuration", str(action)] - if prefix: - cmd = [*prefix, *cmd] - - run_wrapper( - cmd, - env={ - "LOCALE_ARCHIVE": PRESERVE_ENV, - "NIXOS_NO_CHECK": PRESERVE_ENV, - "NIXOS_INSTALL_BOOTLOADER": "1" if install_bootloader else "0", - }, - remote=target_host, - sudo=sudo, - ) - - -def _run_action_with_systemd( - action: Literal[Action.SWITCH, Action.BOOT, Action.TEST, Action.DRY_ACTIVATE], - path_to_config: Path, - install_bootloader: bool, - target_host: Remote | None, - sudo: bool, -) -> None: - unique_unit_name = SYSTEMD_RUN_UNIT_PREFIX + "-" + uuid.uuid4().hex[:8] - journalctl = run_wrapper_bg( - [ - "journalctl", - "-f", - f"--unit={unique_unit_name}", - "--output=cat", - ], - remote=target_host, - sudo=sudo, - ) - - try: - _run_action( - action=action, - path_to_config=path_to_config, - install_bootloader=install_bootloader, - target_host=target_host, - sudo=sudo, - prefix=[*SYSTEMD_RUN_CMD_PREFIX, f"--unit={unique_unit_name}"], - ) - except KeyboardInterrupt: - run_wrapper( - ["systemctl", "stop", unique_unit_name], - remote=target_host, - sudo=sudo, - ) - raise - finally: - journalctl.terminate() - - def switch_to_configuration( path_to_config: Path, action: Literal[Action.SWITCH, Action.BOOT, Action.TEST, Action.DRY_ACTIVATE], @@ -759,26 +693,29 @@ def switch_to_configuration( if not path_to_config.exists(): raise NixOSRebuildError(f"specialisation not found: {specialisation}") - if _has_systemd(target_host): - _run_action_with_systemd( - action=action, - path_to_config=path_to_config, - install_bootloader=install_bootloader, - target_host=target_host, - sudo=sudo, - ) - else: + r = run_wrapper( + ["test", "-d", "/run/systemd/system"], + remote=target_host, + check=False, + ) + cmd = SWITCH_TO_CONFIGURATION_CMD_PREFIX + if r.returncode: logger.debug( "skipping systemd-run to switch configuration since systemd is " "not working in target host" ) - _run_action( - action=action, - path_to_config=path_to_config, - install_bootloader=install_bootloader, - target_host=target_host, - sudo=sudo, - ) + cmd = [] + + run_wrapper( + [*cmd, path_to_config / "bin/switch-to-configuration", str(action)], + env={ + "LOCALE_ARCHIVE": PRESERVE_ENV, + "NIXOS_NO_CHECK": PRESERVE_ENV, + "NIXOS_INSTALL_BOOTLOADER": "1" if install_bootloader else "0", + }, + remote=target_host, + sudo=sudo, + ) def upgrade_channels(all_channels: bool = False, sudo: bool = False) -> None: diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/process.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/process.py index 2a24eca5baad..9f6baaa178a9 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/process.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/process.py @@ -9,7 +9,7 @@ from collections.abc import Mapping, Sequence from dataclasses import dataclass from enum import Enum from ipaddress import AddressValueError, IPv6Address -from typing import Final, Literal, NamedTuple, Self, TextIO, TypedDict, Unpack, override +from typing import Final, Literal, Self, TextIO, TypedDict, Unpack, override from . import tmpdir @@ -46,6 +46,7 @@ class Remote: host: str opts: list[str] sudo_password: str | None + store_type: str @classmethod def from_arg( @@ -57,13 +58,18 @@ class Remote: if not host: return None + try: + store_type, host = host.split("://", 1) + except ValueError: + store_type = "ssh" + opts = shlex.split(os.getenv("NIX_SSHOPTS", "")) if validate_opts: cls._validate_opts(opts, ask_sudo_password) sudo_password = None if ask_sudo_password: sudo_password = getpass.getpass(f"[sudo] password for {host}: ") - return cls(host, opts, sudo_password) + return cls(host, opts, sudo_password, store_type) @staticmethod def _validate_opts(opts: list[str], ask_sudo_password: bool | None) -> None: @@ -117,18 +123,17 @@ def cleanup_ssh() -> None: atexit.register(cleanup_ssh) -class _RunParams(NamedTuple): - args: Args - popen_env: dict[str, str] | None - process_input: str | None - - -def _build_run_params( +def run_wrapper( args: Args, - env: Mapping[str, EnvValue] | None, - remote: Remote | None, - sudo: bool, -) -> _RunParams: + *, + check: bool = True, + env: Mapping[str, EnvValue] | None = None, + append_local_env: Mapping[str, str] | None = None, + remote: Remote | None = None, + sudo: bool = False, + **kwargs: Unpack[RunKwargs], +) -> subprocess.CompletedProcess[str]: + "Wrapper around `subprocess.run` that supports extra functionality." process_input = None run_args: list[Arg] = list(args) final_args: list[Arg] @@ -190,64 +195,9 @@ def _build_run_params( final_args = run_args popen_env = None if env is None else resolved_env - return _RunParams(final_args, popen_env, process_input) - - -def run_wrapper_bg( - args: Args, - env: Mapping[str, EnvValue] | None = None, - remote: Remote | None = None, - sudo: bool = False, -) -> subprocess.Popen[str]: - "Wrapper around `subprocess.Popen` that supports extra functionality." - (final_args, popen_env, process_input) = _build_run_params(args, env, remote, sudo) - - logger.debug( - "calling Popen with args=%r", - _sanitize_env_run_args(list(final_args)), - ) - - if process_input: - stdin = subprocess.PIPE - else: - stdin = None - - r = subprocess.Popen( - final_args, - env=popen_env, - stdin=stdin, - # Hope nobody is using NixOS with non-UTF8 encodings, but - # "surrogateescape" should still work in those systems. - text=True, - errors="surrogateescape", - ) - - if r.stdin and process_input: - r.stdin.write(process_input) - r.stdin.write("\n") - r.stdin.close() - - return r - - -def run_wrapper( - args: Args, - *, - check: bool = True, - env: Mapping[str, EnvValue] | None = None, - append_local_env: Mapping[str, str] | None = None, - remote: Remote | None = None, - sudo: bool = False, - **kwargs: Unpack[RunKwargs], -) -> subprocess.CompletedProcess[str]: - "Wrapper around `subprocess.run` that supports extra functionality." - (final_args, popen_env, process_input) = _build_run_params( - list(args), env, remote, sudo - ) - logger.debug( "calling run with args=%r, kwargs=%r, env=%r, append_local_env=%r", - _sanitize_env_run_args(list(final_args)), + _sanitize_env_run_args(remote_run_args if remote else run_args), kwargs, env, append_local_env, diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_main.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_main.py index 986f6883a42d..7f51c09d7497 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_main.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_main.py @@ -166,15 +166,8 @@ def test_parse_args() -> None: {"NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM": "1"}, clear=True, ) -@patch("uuid.uuid4") @patch("subprocess.run", autospec=True) -@patch("subprocess.Popen") -def test_execute_nix_boot( - mock_popen: Mock, mock_run: Mock, mock_uuid4: Mock, tmp_path: Path -) -> None: - test_uuid = uuid.UUID("58fe9784-f60a-42bc-aa94-eb8f1a7e5c17") - mock_uuid4.return_value = test_uuid - +def test_execute_nix_boot(mock_run: Mock, tmp_path: Path) -> None: nixpkgs_path = tmp_path / "nixpkgs" (nixpkgs_path / ".git").mkdir(parents=True) config_path = tmp_path / "test" @@ -245,8 +238,7 @@ def test_execute_nix_boot( ), call( [ - *nr.nix.SYSTEMD_RUN_CMD_PREFIX, - f"--unit={nr.nix.SYSTEMD_RUN_UNIT_PREFIX}-{test_uuid.hex[:8]}", + *nr.nix.SWITCH_TO_CONFIGURATION_CMD_PREFIX, config_path / "bin/switch-to-configuration", "boot", ], @@ -267,8 +259,7 @@ def test_execute_nix_boot( # https://github.com/NixOS/nixpkgs/issues/437872 @patch.dict(os.environ, {}, clear=True) @patch("subprocess.run", autospec=True) -@patch("subprocess.Popen") -def test_execute_nix_build(mock_popen: Mock, mock_run: Mock, tmp_path: Path) -> None: +def test_execute_nix_build(mock_run: Mock, tmp_path: Path) -> None: config_path = tmp_path / "test" config_path.touch() @@ -310,8 +301,7 @@ def test_execute_nix_build(mock_popen: Mock, mock_run: Mock, tmp_path: Path) -> @patch.dict(os.environ, {}, clear=True) @patch("subprocess.run", autospec=True) -@patch("subprocess.Popen") -def test_execute_nix_build_vm(mock_popen: Mock, mock_run: Mock, tmp_path: Path) -> None: +def test_execute_nix_build_vm(mock_run: Mock, tmp_path: Path) -> None: config_path = tmp_path / "test" config_path.touch() @@ -360,10 +350,7 @@ def test_execute_nix_build_vm(mock_popen: Mock, mock_run: Mock, tmp_path: Path) @patch.dict(os.environ, {}, clear=True) @patch("subprocess.run", autospec=True) -@patch("subprocess.Popen") -def test_execute_nix_build_image_flake( - mock_popen: Mock, mock_run: Mock, tmp_path: Path -) -> None: +def test_execute_nix_build_image_flake(mock_run: Mock, tmp_path: Path) -> None: config_path = tmp_path / "test" config_path.touch() @@ -445,18 +432,11 @@ def test_execute_nix_build_image_flake( {"NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM": "1"}, clear=True, ) -@patch("uuid.uuid4") @patch("subprocess.run", autospec=True) -@patch("subprocess.Popen") -def test_execute_nix_switch_flake( - mock_popen: Mock, mock_run: Mock, mock_uuid4: Mock, tmp_path: Path -) -> None: +def test_execute_nix_switch_flake(mock_run: Mock, tmp_path: Path) -> None: config_path = tmp_path / "test" config_path.touch() - test_uuid = uuid.UUID("58fe9784-f60a-42bc-aa94-eb8f1a7e5c17") - mock_uuid4.return_value = test_uuid - def run_side_effect(args: list[str], **kwargs: Any) -> CompletedProcess[str]: if args[0] == "nix": return CompletedProcess([], 0, str(config_path)) @@ -526,8 +506,7 @@ def test_execute_nix_switch_flake( "env", "-i", "NIXOS_INSTALL_BOOTLOADER=1", - *nr.nix.SYSTEMD_RUN_CMD_PREFIX, - f"--unit={nr.nix.SYSTEMD_RUN_UNIT_PREFIX}-{test_uuid.hex[:8]}", + *nr.nix.SWITCH_TO_CONFIGURATION_CMD_PREFIX, config_path / "bin/switch-to-configuration", "switch", ], @@ -544,13 +523,11 @@ def test_execute_nix_switch_flake( clear=True, ) @patch("subprocess.run", autospec=True) -@patch("subprocess.Popen") @patch("uuid.uuid4", autospec=True) @patch(get_qualified_name(nr.services.cleanup_ssh), autospec=True) def test_execute_nix_switch_build_target_host( mock_cleanup_ssh: Mock, mock_uuid4: Mock, - mock_popen: Mock, mock_run: Mock, tmp_path: Path, ) -> None: @@ -574,8 +551,7 @@ def test_execute_nix_switch_build_target_host( return CompletedProcess([], 0) mock_run.side_effect = run_side_effect - test_uuid = uuid.UUID("58fe9784-f60a-42bc-aa94-eb8f1a7e5c17") - mock_uuid4.side_effect = [uuid.UUID(int=0), uuid.UUID(int=1), test_uuid] + mock_uuid4.return_value = uuid.UUID(int=0) nr.execute( [ @@ -668,7 +644,7 @@ def test_execute_nix_switch_build_target_host( "--realise", str(config_path), "--add-root", - "/tmp/tmpdir/00000000000000000000000000000001", + "/tmp/tmpdir/00000000000000000000000000000000", ], check=True, stdout=PIPE, @@ -772,8 +748,7 @@ def test_execute_nix_switch_build_target_host( "-c", """'exec env -i PATH="${PATH-}" LOCALE_ARCHIVE="${LOCALE_ARCHIVE-}" NIXOS_NO_CHECK="${NIXOS_NO_CHECK-}" NIXOS_INSTALL_BOOTLOADER=0 "$@"'""", "sh", - *nr.nix.SYSTEMD_RUN_CMD_PREFIX, - f"--unit={nr.nix.SYSTEMD_RUN_UNIT_PREFIX}-{test_uuid.hex[:8]}", + *nr.nix.SWITCH_TO_CONFIGURATION_CMD_PREFIX, str(config_path / "bin/switch-to-configuration"), "switch", ], @@ -789,20 +764,13 @@ def test_execute_nix_switch_build_target_host( {"NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM": "1"}, clear=True, ) -@patch("uuid.uuid4") @patch("subprocess.run", autospec=True) -@patch("subprocess.Popen") @patch(get_qualified_name(nr.services.cleanup_ssh), autospec=True) def test_execute_nix_switch_flake_target_host( mock_cleanup_ssh: Mock, - mock_popen: Mock, mock_run: Mock, - mock_uuid4: Mock, tmp_path: Path, ) -> None: - test_uuid = uuid.UUID("58fe9784-f60a-42bc-aa94-eb8f1a7e5c17") - mock_uuid4.return_value = test_uuid - config_path = tmp_path / "test" config_path.touch() @@ -897,8 +865,7 @@ def test_execute_nix_switch_flake_target_host( "-c", """'exec env -i PATH="${PATH-}" LOCALE_ARCHIVE="${LOCALE_ARCHIVE-}" NIXOS_NO_CHECK="${NIXOS_NO_CHECK-}" NIXOS_INSTALL_BOOTLOADER=0 "$@"'""", "sh", - *nr.nix.SYSTEMD_RUN_CMD_PREFIX, - f"--unit={nr.nix.SYSTEMD_RUN_UNIT_PREFIX}-{test_uuid.hex[:8]}", + *nr.nix.SWITCH_TO_CONFIGURATION_CMD_PREFIX, str(config_path / "bin/switch-to-configuration"), "switch", ], @@ -914,20 +881,13 @@ def test_execute_nix_switch_flake_target_host( {"NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM": "1"}, clear=True, ) -@patch("uuid.uuid4") @patch("subprocess.run", autospec=True) -@patch("subprocess.Popen") @patch(get_qualified_name(nr.services.cleanup_ssh), autospec=True) def test_execute_nix_switch_flake_build_host( mock_cleanup_ssh: Mock, - mock_popen: Mock, mock_run: Mock, - mock_uuid4: Mock, tmp_path: Path, ) -> None: - test_uuid = uuid.UUID("58fe9784-f60a-42bc-aa94-eb8f1a7e5c17") - mock_uuid4.return_value = test_uuid - config_path = tmp_path / "test" config_path.touch() @@ -1024,8 +984,7 @@ def test_execute_nix_switch_flake_build_host( ), call( [ - *nr.nix.SYSTEMD_RUN_CMD_PREFIX, - f"--unit={nr.nix.SYSTEMD_RUN_UNIT_PREFIX}-{test_uuid.hex[:8]}", + *nr.nix.SWITCH_TO_CONFIGURATION_CMD_PREFIX, config_path / "bin/switch-to-configuration", "switch", ], @@ -1037,10 +996,7 @@ def test_execute_nix_switch_flake_build_host( @patch("subprocess.run", autospec=True) -@patch("subprocess.Popen") -def test_execute_switch_rollback( - mock_popen: Mock, mock_run: Mock, tmp_path: Path -) -> None: +def test_execute_switch_rollback(mock_run: Mock, tmp_path: Path) -> None: nixpkgs_path = tmp_path / "nixpkgs" (nixpkgs_path / ".git").mkdir(parents=True) @@ -1117,8 +1073,7 @@ def test_execute_switch_rollback( @patch("subprocess.run", autospec=True) -@patch("subprocess.Popen") -def test_execute_build(mock_popen: Mock, mock_run: Mock, tmp_path: Path) -> None: +def test_execute_build(mock_run: Mock, tmp_path: Path) -> None: config_path = tmp_path / "test" config_path.touch() mock_run.side_effect = [ @@ -1147,9 +1102,8 @@ def test_execute_build(mock_popen: Mock, mock_run: Mock, tmp_path: Path) -> None @patch("subprocess.run", autospec=True) -@patch("subprocess.Popen") def test_execute_build_dry_run_build_and_target_remote( - mock_popen: Mock, mock_run: Mock, tmp_path: Path + mock_run: Mock, tmp_path: Path ) -> None: config_path = tmp_path / "test" config_path.touch() @@ -1220,8 +1174,7 @@ def test_execute_build_dry_run_build_and_target_remote( @patch("subprocess.run", autospec=True) -@patch("subprocess.Popen") -def test_execute_test_flake(mock_popen: Mock, mock_run: Mock, tmp_path: Path) -> None: +def test_execute_test_flake(mock_run: Mock, tmp_path: Path) -> None: config_path = tmp_path / "test" config_path.touch() @@ -1271,13 +1224,11 @@ def test_execute_test_flake(mock_popen: Mock, mock_run: Mock, tmp_path: Path) -> @patch("subprocess.run", autospec=True) -@patch("subprocess.Popen") @patch("pathlib.Path.exists", autospec=True, return_value=True) @patch("pathlib.Path.mkdir", autospec=True) def test_execute_test_rollback( mock_path_mkdir: Mock, mock_path_exists: Mock, - mock_popen: Mock, mock_run: Mock, ) -> None: def run_side_effect(args: list[str], **kwargs: Any) -> CompletedProcess[str]: @@ -1340,15 +1291,8 @@ def test_execute_test_rollback( {"NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM": "1"}, clear=True, ) -@patch("uuid.uuid4", autospec=True) @patch("subprocess.run", autospec=True) -@patch("subprocess.Popen") -def test_execute_switch_store_path( - mock_popen: Mock, mock_run: Mock, mock_uuid4: Mock, tmp_path: Path -) -> None: - test_uuid = uuid.UUID("58fe9784-f60a-42bc-aa94-eb8f1a7e5c17") - mock_uuid4.return_value = test_uuid - +def test_execute_switch_store_path(mock_run: Mock, tmp_path: Path) -> None: config_path = tmp_path / "test-system" config_path.mkdir() @@ -1386,8 +1330,7 @@ def test_execute_switch_store_path( ), call( [ - *nr.nix.SYSTEMD_RUN_CMD_PREFIX, - f"--unit={nr.nix.SYSTEMD_RUN_UNIT_PREFIX}-{test_uuid.hex[:8]}", + *nr.nix.SWITCH_TO_CONFIGURATION_CMD_PREFIX, config_path / "bin/switch-to-configuration", "switch", ], @@ -1406,20 +1349,13 @@ def test_execute_switch_store_path( @patch.dict(os.environ, {}, clear=True) -@patch("uuid.uuid4") @patch("subprocess.run", autospec=True) @patch(get_qualified_name(nr.services.cleanup_ssh), autospec=True) -@patch("subprocess.Popen") def test_execute_switch_store_path_target_host( - mock_popen: Mock, mock_cleanup_ssh: Mock, mock_run: Mock, - mock_uuid4: Mock, tmp_path: Path, ) -> None: - test_uuid = uuid.UUID("58fe9784-f60a-42bc-aa94-eb8f1a7e5c17") - mock_uuid4.return_value = test_uuid - config_path = tmp_path / "test-system" config_path.mkdir() @@ -1512,8 +1448,7 @@ def test_execute_switch_store_path_target_host( "-c", """'exec env -i PATH="${PATH-}" LOCALE_ARCHIVE="${LOCALE_ARCHIVE-}" NIXOS_NO_CHECK="${NIXOS_NO_CHECK-}" NIXOS_INSTALL_BOOTLOADER=0 "$@"'""", "sh", - *nr.nix.SYSTEMD_RUN_CMD_PREFIX, - f"--unit={nr.nix.SYSTEMD_RUN_UNIT_PREFIX}-{test_uuid.hex[:8]}", + *nr.nix.SWITCH_TO_CONFIGURATION_CMD_PREFIX, str(config_path / "bin/switch-to-configuration"), "switch", ], diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_models.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_models.py index bc6bbdee3dda..21c957b9d821 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_models.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_models.py @@ -43,7 +43,7 @@ def test_flake_parse(mock_node: Mock, tmpdir: Path, monkeypatch: MonkeyPatch) -> autospec=True, return_value=subprocess.CompletedProcess([], 0, stdout="remote\n"), ): - target_host = m.Remote("target@remote", [], None) + target_host = m.Remote("target@remote", [], None, "ssh") assert m.Flake.parse("/path/to/flake", target_host) == m.Flake( "/path/to/flake", 'nixosConfigurations."remote"' ) @@ -162,9 +162,9 @@ def test_flake_from_arg( return_value=subprocess.CompletedProcess([], 0, "remote-hostname\n"), ), ): - assert m.Flake.from_arg("/path/to", m.Remote("user@host", [], None)) == m.Flake( - "/path/to", 'nixosConfigurations."remote-hostname"' - ) + assert m.Flake.from_arg( + "/path/to", m.Remote("user@host", [], None, "ssh") + ) == m.Flake("/path/to", 'nixosConfigurations."remote-hostname"') @patch("pathlib.Path.mkdir", autospec=True) diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_nix.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_nix.py index 4978be181ee0..28948d1eaaff 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_nix.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_nix.py @@ -2,7 +2,7 @@ import sys import textwrap import uuid from pathlib import Path -from subprocess import PIPE, CompletedProcess, Popen +from subprocess import PIPE, CompletedProcess from typing import Any from unittest.mock import ANY, Mock, call, patch @@ -83,7 +83,7 @@ def test_build_flake(mock_run: Mock, monkeypatch: MonkeyPatch, tmpdir: Path) -> def test_build_remote( mock_uuid4: Mock, mock_run: Mock, monkeypatch: MonkeyPatch ) -> None: - build_host = m.Remote("user@host", [], None) + build_host = m.Remote("user@host", [], None, "ssh") monkeypatch.setenv("NIX_SSHOPTS", "--ssh opts") def run_wrapper_side_effect( @@ -177,7 +177,7 @@ def test_build_remote_flake( ) -> None: monkeypatch.chdir(tmpdir) flake = m.Flake.parse("/flake.nix#hostname") - build_host = m.Remote("user@host", [], None) + build_host = m.Remote("user@host", [], None, "ssh") monkeypatch.setenv("NIX_SSHOPTS", "--ssh opts") assert n.build_remote_flake( @@ -237,8 +237,9 @@ def test_copy_closure(monkeypatch: MonkeyPatch) -> None: n.copy_closure(closure, None) mock_run.assert_not_called() - target_host = m.Remote("user@target.host", [], None) - build_host = m.Remote("user@build.host", [], None) + target_host = m.Remote("user@target.host", [], None, "ssh") + build_host = m.Remote("user@build.host", [], None, "ssh") + target_host_ng = m.Remote("user@target.host", [], None, "ssh-ng") with patch(get_qualified_name(n.run_wrapper, n), autospec=True) as mock_run: n.copy_closure(closure, target_host) mock_run.assert_called_with( @@ -246,6 +247,21 @@ def test_copy_closure(monkeypatch: MonkeyPatch) -> None: append_local_env={"NIX_SSHOPTS": " ".join(p.SSH_DEFAULT_OPTS)}, ) + with patch(get_qualified_name(n.run_wrapper, n), autospec=True) as mock_run: + n.copy_closure(closure, target_host_ng) + mock_run.assert_called_with( + [ + "nix", + "--extra-experimental-features", + "nix-command flakes", + "copy", + "--to", + "ssh-ng://user@target.host", + closure, + ], + append_local_env={"NIX_SSHOPTS": " ".join(p.SSH_DEFAULT_OPTS)}, + ) + monkeypatch.setenv("NIX_SSHOPTS", "--ssh build-opt") with patch(get_qualified_name(n.run_wrapper, n), autospec=True) as mock_run: n.copy_closure(closure, None, build_host, {"copy_flag": True}) @@ -458,6 +474,10 @@ def test_get_generations(tmp_path: Path) -> None: (tmp_path / "system-3-link").symlink_to(nixos_path) (tmp_path / "system-2-link").symlink_to(nixos_path) + # An alternate profile; this shouldn't appear. + (tmp_path / "custom").symlink_to(tmp_path / "custom-1-link") + (tmp_path / "custom-1-link").symlink_to(nixos_path) + assert n.get_generations(m.Profile("system", tmp_path / "system")) == [ m.Generation(id=1, current=False, timestamp=ANY), m.Generation(id=2, current=True, timestamp=ANY), @@ -465,6 +485,27 @@ def test_get_generations(tmp_path: Path) -> None: ] +def test_get_generations_with_profile(tmp_path: Path) -> None: + nixos_path = tmp_path / "nixos-system" + nixos_path.mkdir() + + (tmp_path / "custom").symlink_to(tmp_path / "custom-2-link") + # In the "wrong" order on purpose to make sure we are sorting the results + (tmp_path / "custom-1-link").symlink_to(nixos_path) + (tmp_path / "custom-3-link").symlink_to(nixos_path) + (tmp_path / "custom-2-link").symlink_to(nixos_path) + + # An alternate profile; none of these should appear. + (tmp_path / "system").symlink_to(tmp_path / "system-1-link") + (tmp_path / "system-1-link").symlink_to(nixos_path) + + assert n.get_generations(m.Profile("custom", tmp_path / "custom")) == [ + m.Generation(id=1, current=False, timestamp=ANY), + m.Generation(id=2, current=True, timestamp=ANY), + m.Generation(id=3, current=False, timestamp=ANY), + ] + + def test_get_generations_from_nix_env(tmp_path: Path) -> None: path = tmp_path / "test" path.touch() @@ -493,7 +534,7 @@ def test_get_generations_from_nix_env(tmp_path: Path) -> None: sudo=False, ) - remote = m.Remote("user@host", [], "password") + remote = m.Remote("user@host", [], "password", "ssh") with patch( get_qualified_name(n.run_wrapper, n), autospec=True, return_value=return_value ) as mock_run: @@ -555,7 +596,6 @@ def test_list_generations(mock_get_generations: Mock, tmp_path: Path) -> None: @patch(get_qualified_name(n.run_wrapper, n), autospec=True) def test_diff_closures(mock_run: Mock) -> None: - n.diff_closures( Path("/run/current-system"), Path("/nix/var/nix/profiles/system"), None ) @@ -607,7 +647,7 @@ def test_rollback(mock_run: Mock, tmp_path: Path) -> None: sudo=False, ) - target_host = m.Remote("user@localhost", [], None) + target_host = m.Remote("user@localhost", [], None, "ssh") assert n.rollback(profile, target_host, True) == profile.path mock_run.assert_called_with( ["nix-env", "--rollback", "-p", path], @@ -647,7 +687,7 @@ def test_rollback_temporary_profile(tmp_path: Path) -> None: sudo=False, ) - target_host = m.Remote("user@localhost", [], None) + target_host = m.Remote("user@localhost", [], None, "ssh") assert ( n.rollback_temporary_profile(m.Profile("foo", path), target_host, True) == path.parent / "foo-2083-link" @@ -703,159 +743,139 @@ def test_set_profile(mock_run: Mock) -> None: @patch(get_qualified_name(n.run_wrapper, n), autospec=True) -@patch(get_qualified_name(n.run_wrapper_bg, n), autospec=True) def test_switch_to_configuration_without_systemd_run( - mock_run_bg: Mock, mock_run: Mock, monkeypatch: MonkeyPatch + mock_run: Any, monkeypatch: MonkeyPatch ) -> None: profile_path = Path("/path/to/profile") config_path = Path("/path/to/config") + mock_run.return_value = CompletedProcess([], 1) - proc = Popen(["echo"]) - try: - mock_run_bg.return_value = p - mock_run.return_value = CompletedProcess([], 1) + with monkeypatch.context() as mp: + mp.setenv("LOCALE_ARCHIVE", "") - with monkeypatch.context() as mp: - mp.setenv("LOCALE_ARCHIVE", "") - - n.switch_to_configuration( - profile_path, - m.Action.SWITCH, - sudo=False, - target_host=None, - specialisation=None, - install_bootloader=False, - ) - mock_run.assert_called_with( - [profile_path / "bin/switch-to-configuration", "switch"], - env={ - "LOCALE_ARCHIVE": p.PRESERVE_ENV, - "NIXOS_NO_CHECK": p.PRESERVE_ENV, - "NIXOS_INSTALL_BOOTLOADER": "0", - }, + n.switch_to_configuration( + profile_path, + m.Action.SWITCH, sudo=False, - remote=None, + target_host=None, + specialisation=None, + install_bootloader=False, ) + mock_run.assert_called_with( + [profile_path / "bin/switch-to-configuration", "switch"], + env={ + "LOCALE_ARCHIVE": p.PRESERVE_ENV, + "NIXOS_NO_CHECK": p.PRESERVE_ENV, + "NIXOS_INSTALL_BOOTLOADER": "0", + }, + sudo=False, + remote=None, + ) - with pytest.raises(m.NixOSRebuildError) as e: - n.switch_to_configuration( - config_path, - m.Action.BOOT, - sudo=False, - target_host=None, - specialisation="special", - ) - assert ( - str(e.value) - == "error: '--specialisation' can only be used with 'switch' and 'test'" + with pytest.raises(m.NixOSRebuildError) as e: + n.switch_to_configuration( + config_path, + m.Action.BOOT, + sudo=False, + target_host=None, + specialisation="special", ) + assert ( + str(e.value) + == "error: '--specialisation' can only be used with 'switch' and 'test'" + ) - target_host = m.Remote("user@localhost", [], None) - with monkeypatch.context() as mp: - mp.setenv("LOCALE_ARCHIVE", "/path/to/locale") - mp.setenv("PATH", "/path/to/bin") - mp.setattr(Path, Path.exists.__name__, lambda self: True) + target_host = m.Remote("user@localhost", [], None, "ssh") + with monkeypatch.context() as mp: + mp.setenv("LOCALE_ARCHIVE", "/path/to/locale") + mp.setenv("PATH", "/path/to/bin") + mp.setattr(Path, Path.exists.__name__, lambda self: True) - n.switch_to_configuration( - Path("/path/to/config"), - m.Action.TEST, - sudo=True, - target_host=target_host, - install_bootloader=True, - specialisation="special", - ) - mock_run.assert_called_with( - [ - config_path / "specialisation/special/bin/switch-to-configuration", - "test", - ], - env={ - "LOCALE_ARCHIVE": p.PRESERVE_ENV, - "NIXOS_NO_CHECK": p.PRESERVE_ENV, - "NIXOS_INSTALL_BOOTLOADER": "1", - }, + n.switch_to_configuration( + Path("/path/to/config"), + m.Action.TEST, sudo=True, - remote=target_host, + target_host=target_host, + install_bootloader=True, + specialisation="special", ) - finally: - proc.communicate(timeout=1) + mock_run.assert_called_with( + [ + config_path / "specialisation/special/bin/switch-to-configuration", + "test", + ], + env={ + "LOCALE_ARCHIVE": p.PRESERVE_ENV, + "NIXOS_NO_CHECK": p.PRESERVE_ENV, + "NIXOS_INSTALL_BOOTLOADER": "1", + }, + sudo=True, + remote=target_host, + ) -@patch("uuid.uuid4") @patch(get_qualified_name(n.run_wrapper, n), autospec=True) -@patch(get_qualified_name(n.run_wrapper_bg, n), autospec=True) def test_switch_to_configuration_with_systemd_run( - mock_run_bg: Mock, mock_run: Mock, mock_uuid4: Mock, monkeypatch: MonkeyPatch + mock_run: Mock, monkeypatch: MonkeyPatch ) -> None: - test_uuid = uuid.UUID("58fe9784-f60a-42bc-aa94-eb8f1a7e5c17") - mock_uuid4.return_value = test_uuid - profile_path = Path("/path/to/profile") config_path = Path("/path/to/config") + mock_run.return_value = CompletedProcess([], 0) - proc = Popen(["echo"]) - try: - mock_run_bg.return_value = proc - mock_run.return_value = CompletedProcess([], 0) + with monkeypatch.context() as mp: + mp.setenv("LOCALE_ARCHIVE", "") - with monkeypatch.context() as mp: - mp.setenv("LOCALE_ARCHIVE", "") - - n.switch_to_configuration( - profile_path, - m.Action.SWITCH, - sudo=False, - target_host=None, - specialisation=None, - install_bootloader=False, - ) - mock_run.assert_called_with( - [ - *n.SYSTEMD_RUN_CMD_PREFIX, - f"--unit={n.SYSTEMD_RUN_UNIT_PREFIX}-{test_uuid.hex[:8]}", - profile_path / "bin/switch-to-configuration", - "switch", - ], - env={ - "LOCALE_ARCHIVE": p.PRESERVE_ENV, - "NIXOS_NO_CHECK": p.PRESERVE_ENV, - "NIXOS_INSTALL_BOOTLOADER": "0", - }, + n.switch_to_configuration( + profile_path, + m.Action.SWITCH, sudo=False, - remote=None, + target_host=None, + specialisation=None, + install_bootloader=False, ) + mock_run.assert_called_with( + [ + *n.SWITCH_TO_CONFIGURATION_CMD_PREFIX, + profile_path / "bin/switch-to-configuration", + "switch", + ], + env={ + "LOCALE_ARCHIVE": p.PRESERVE_ENV, + "NIXOS_NO_CHECK": p.PRESERVE_ENV, + "NIXOS_INSTALL_BOOTLOADER": "0", + }, + sudo=False, + remote=None, + ) - target_host = m.Remote("user@localhost", [], None) - with monkeypatch.context() as mp: - mp.setenv("LOCALE_ARCHIVE", "/path/to/locale") - mp.setenv("PATH", "/path/to/bin") - mp.setattr(Path, Path.exists.__name__, lambda self: True) + target_host = m.Remote("user@localhost", [], None, "ssh") + with monkeypatch.context() as mp: + mp.setenv("LOCALE_ARCHIVE", "/path/to/locale") + mp.setenv("PATH", "/path/to/bin") + mp.setattr(Path, Path.exists.__name__, lambda self: True) - n.switch_to_configuration( - Path("/path/to/config"), - m.Action.TEST, - sudo=True, - target_host=target_host, - install_bootloader=True, - specialisation="special", - ) - mock_run.assert_called_with( - [ - *n.SYSTEMD_RUN_CMD_PREFIX, - f"--unit={n.SYSTEMD_RUN_UNIT_PREFIX}-{test_uuid.hex[:8]}", - config_path / "specialisation/special/bin/switch-to-configuration", - "test", - ], - env={ - "LOCALE_ARCHIVE": p.PRESERVE_ENV, - "NIXOS_NO_CHECK": p.PRESERVE_ENV, - "NIXOS_INSTALL_BOOTLOADER": "1", - }, + n.switch_to_configuration( + Path("/path/to/config"), + m.Action.TEST, sudo=True, - remote=target_host, + target_host=target_host, + install_bootloader=True, + specialisation="special", ) - finally: - proc.communicate(timeout=1) + mock_run.assert_called_with( + [ + *n.SWITCH_TO_CONFIGURATION_CMD_PREFIX, + config_path / "specialisation/special/bin/switch-to-configuration", + "test", + ], + env={ + "LOCALE_ARCHIVE": p.PRESERVE_ENV, + "NIXOS_NO_CHECK": p.PRESERVE_ENV, + "NIXOS_INSTALL_BOOTLOADER": "1", + }, + sudo=True, + remote=target_host, + ) @patch( diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_process.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_process.py index 9374e9d1620e..3780b21f26d5 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_process.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_process.py @@ -112,7 +112,7 @@ def test_run_wrapper(mock_run: Any) -> None: p.run_wrapper( ["test", "--with", "some flags"], check=True, - remote=m.Remote("user@localhost", ["--ssh", "opt"], "password"), + remote=m.Remote("user@localhost", ["--ssh", "opt"], "password", "ssh"), ) mock_run.assert_called_with( [ @@ -142,7 +142,7 @@ def test_run_wrapper(mock_run: Any) -> None: check=True, sudo=True, env={"FOO": "bar"}, - remote=m.Remote("user@localhost", ["--ssh", "opt"], "password"), + remote=m.Remote("user@localhost", ["--ssh", "opt"], "password", "ssh"), ) mock_run.assert_called_with( [ @@ -181,7 +181,7 @@ def test__kill_long_running_ssh_process(mock_run: Any) -> None: "build", "/nix/store/la0c8nmpr9xfclla0n4f3qq9iwgdrq4g-nixos-system-sankyuu-nixos-25.05.20250424.f771eb4.drv^*", ], - m.Remote("user@localhost", opts=[], sudo_password=None), + m.Remote("user@localhost", opts=[], sudo_password=None, store_type="ssh"), ) mock_run.assert_called_with( [ @@ -208,6 +208,7 @@ def test_remote_from_name(monkeypatch: MonkeyPatch) -> None: "user@localhost", opts=[], sudo_password=None, + store_type="ssh", ) with patch("getpass.getpass", autospec=True, return_value="password"): @@ -216,11 +217,12 @@ def test_remote_from_name(monkeypatch: MonkeyPatch) -> None: "user@localhost", opts=["-f", "foo", "-b", "bar", "-t"], sudo_password="password", + store_type="ssh", ) def test_ssh_host() -> None: - remotes = { + ssh_remotes = { "user@[fe80::1%25eth0]": "user@fe80::1%eth0", "[fe80::c98b%25enp4s0]": "fe80::c98b%enp4s0", "user@[2001::5fce:a:198]": "user@2001::5fce:a:198", @@ -231,12 +233,23 @@ def test_ssh_host() -> None: "localhost": "localhost", "user@example.org": "user@example.org", "example.org": "example.org", + "ssh://explicit-store@localhost": "explicit-store@localhost", + } + ssh_ng_remotes = { + "ssh-ng://example.org": "example.org", } - for host_input, expected in remotes.items(): + for host_input, expected in ssh_remotes.items(): remote = m.Remote.from_arg(host_input, None, False) assert remote is not None assert remote.ssh_host() == expected + assert remote.store_type == "ssh" + + for host_input, expected in ssh_ng_remotes.items(): + remote = m.Remote.from_arg(host_input, None, False) + assert remote is not None + assert remote.ssh_host() == expected + assert remote.store_type == "ssh-ng" @patch("subprocess.run", autospec=True) @@ -275,7 +288,7 @@ def test_custom_sudo_args(mock_run: Any) -> None: ["test"], check=False, sudo=True, - remote=m.Remote("user@localhost", [], None), + remote=m.Remote("user@localhost", [], None, "ssh"), ) mock_run.assert_called_with( [ diff --git a/pkgs/by-name/pa/paperless-ngx/package.nix b/pkgs/by-name/pa/paperless-ngx/package.nix index b1d984507375..9b92b50eb7d7 100644 --- a/pkgs/by-name/pa/paperless-ngx/package.nix +++ b/pkgs/by-name/pa/paperless-ngx/package.nix @@ -29,13 +29,13 @@ lndir, }: let - version = "2.20.10"; + version = "2.20.11"; src = fetchFromGitHub { owner = "paperless-ngx"; repo = "paperless-ngx"; tag = "v${version}"; - hash = "sha256-4kc1LyqcVKHQ/WnsZOL0zanbLIv27CGGgNZXVFBwCgQ="; + hash = "sha256-Bn5k6h80nSNxWYsIpqVLXp+udzxDCY8f/jbgDvyATM0="; }; python = python3.override { diff --git a/pkgs/by-name/pi/piday25/package.nix b/pkgs/by-name/pi/piday25/package.nix index 38142f4bd7bb..4c10f40f9a2e 100644 --- a/pkgs/by-name/pi/piday25/package.nix +++ b/pkgs/by-name/pi/piday25/package.nix @@ -7,13 +7,13 @@ rustPlatform.buildRustPackage { pname = "piday25"; - version = "0-unstable-2025-03-13"; + version = "0-unstable-2026-03-18"; src = fetchFromGitHub { owner = "elkasztano"; repo = "piday25"; - rev = "68b417a3016c58a2948cb3b39c9bde985d82bdb8"; - hash = "sha256-58ZBRmB990Tp+/nkuRZA+8cjCRFUBzdzu93Sk5uvKOE="; + rev = "3fdeb37e33572c0924fc8f23a62824df8f6a9496"; + hash = "sha256-iNc6NUEekk793j3Ob02H9NB4SH1anYsWzixr8OiglOE="; }; cargoHash = "sha256-3uztB5/VevFyEz3S+VlAUPgDrNDJcwaTnHuXXYAX+MY="; diff --git a/pkgs/by-name/pk/pkl-lsp/deps.json b/pkgs/by-name/pk/pkl-lsp/deps.json new file mode 100644 index 000000000000..fe77d123981d --- /dev/null +++ b/pkgs/by-name/pk/pkl-lsp/deps.json @@ -0,0 +1,1021 @@ +{ + "!comment": "This is a nixpkgs Gradle dependency lockfile. For more details, refer to the Gradle section in the nixpkgs manual.", + "!version": 1, + "https://plugins.gradle.org/m2": { + "com/diffplug/durian#durian-collect/1.2.0": { + "jar": "sha256-sZTAuIAhzBFsIcHcdvScLB/hda9by3TIume527+aSMw=", + "pom": "sha256-i7diCGoKT9KmRzu/kFx0R2OvodWaVjD3O7BLeHLAn/M=" + }, + "com/diffplug/durian#durian-core/1.2.0": { + "jar": "sha256-F+0KrLOjwWMjMyFou96thpTzKACytH1p1KTEmxFNXa4=", + "pom": "sha256-hwMg6QdVNxsBeW/oG6Ul/R3ui3A0b1VFUe7dQonwtmI=" + }, + "com/diffplug/durian#durian-io/1.2.0": { + "jar": "sha256-CV/R3HeIjAc/C+OaAYFW7lJnInmLCd6eKF7yE14W6sQ=", + "pom": "sha256-NQkZQkMk4nUKPdwvobzmqQrIziklaYpgqbTR1uSSL/4=" + }, + "com/diffplug/durian#durian-swt.os/4.2.2": { + "jar": "sha256-a1Mca0vlgaizLq2GHdwVwsk7IMZl+00z4DgUg8JERfQ=", + "module": "sha256-rVlQLGknZu48M0vkliigDctNka4aSPJjLitxUStDXPk=", + "pom": "sha256-GzxJFP1eLM4pZq1wdWY5ZBFFwdNCB3CTV4Py3yY2kIU=" + }, + "com/diffplug/spotless#com.diffplug.spotless.gradle.plugin/6.25.0": { + "pom": "sha256-9FyCsS+qzYWs1HTrppkyL6XeqIQIskfQ5L3pQSkIIjo=" + }, + "com/diffplug/spotless#spotless-lib-extra/2.45.0": { + "jar": "sha256-YCy7zTgo7pz7LjCn+bMDNcaScTB3FBTUzdKU0h/ly2c=", + "module": "sha256-9pnkNfTlzgPbYJpHaO6wNj1uB8ZfvPrx/GKcTnbuf7A=", + "pom": "sha256-5x2LkRDdSNLn9KVLi/uozlWpbmteu9T0OpJGZJz1b7A=" + }, + "com/diffplug/spotless#spotless-lib/2.45.0": { + "jar": "sha256-sllply4dmAKAyirlKRl+2bMWCq5ItQbPGTXwG9Exhmc=", + "module": "sha256-+x+8+TUAczrHWcp99E8P9mVTEze0LaAS4on/CINNiQ8=", + "pom": "sha256-WKd8IsQLIc8m29tCEwFu9HrM9bBwchfHkyqQ9D+PMNw=" + }, + "com/diffplug/spotless#spotless-plugin-gradle/6.25.0": { + "jar": "sha256-9euQikxdpGKZ51Q/qtoEAtLEt31Yx7Qy1Lblk0mygKM=", + "module": "sha256-RoHRe/PJIF2DeOynBcAAywzJjcx40DATy2iJjGvSx0Q=", + "pom": "sha256-q1ZuPYS2w/rHqPySXy279TzZdZywOvPAfQ3EN9OXqNo=" + }, + "com/fasterxml#oss-parent/55": { + "pom": "sha256-D14Y8rNev22Dn3/VSZcog/aWwhD5rjIwr9LCC6iGwE0=" + }, + "com/fasterxml#oss-parent/61": { + "pom": "sha256-NklRPPWX6RhtoIVZhqjFQ+Er29gF7e75wSTbVt0DZUQ=" + }, + "com/fasterxml#oss-parent/68": { + "pom": "sha256-Jer9ltriQra1pxCPVbLBQBW4KNqlq+I0KJ/W53Shzlc=" + }, + "com/fasterxml/jackson#jackson-bom/2.19.1": { + "pom": "sha256-um1o7qs6HME6d6it4hl/+aMqoc/+rHKEfUm63YLhuc4=" + }, + "com/fasterxml/jackson#jackson-parent/2.19.2": { + "pom": "sha256-Y5orY90F2k44EIEwOYXKrfu3rZ+FsdIyBjj2sR8gg2U=" + }, + "com/fasterxml/woodstox#woodstox-core/7.1.0": { + "jar": "sha256-gSZpIKHNxHMGqKK0cmyZ7Imz+/McJHDk9eR32dhXyp8=", + "pom": "sha256-+ZXFCx0gl18KjW8OUyK8jRPHiuPcGCcXdoQUlypmzIU=" + }, + "com/google/code/gson#gson-parent/2.11.0": { + "pom": "sha256-issfO3Km8CaRasBzW62aqwKT1Sftt7NlMn3vE6k2e3o=" + }, + "com/google/code/gson#gson/2.11.0": { + "jar": "sha256-V5KNblpu3rKr03cKj5W6RNzkXzsjt6ncKzCcWBVSp4s=", + "pom": "sha256-wOVHvqmYiI5uJcWIapDnYicryItSdTQ90sBd7Wyi42A=" + }, + "com/google/errorprone#error_prone_annotations/2.27.0": { + "jar": "sha256-JMkjNyxY410LnxagKJKbua7cd1IYZ8J08r0HNd9bofU=", + "pom": "sha256-TKWjXWEjXhZUmsNG0eNFUc3w/ifoSqV+A8vrJV6k5do=" + }, + "com/google/errorprone#error_prone_parent/2.27.0": { + "pom": "sha256-+oGCnQSVWd9pJ/nJpv1rvQn4tQ5tRzaucsgwC2w9dlQ=" + }, + "com/googlecode/concurrent-trees#concurrent-trees/2.6.1": { + "jar": "sha256-BONySYTipcv1VgbPo3KlvT08XSohUzpwBOPN5Tl2H6U=", + "pom": "sha256-Q8K5sULnBV0fKlgn8QlEkl0idH2XVrMlDAeqtHU4qXE=" + }, + "com/googlecode/javaewah#JavaEWAH/1.2.3": { + "jar": "sha256-1lImlJcTxMYaeE9BxRFn57Axb5N2Q5jrup5DNrPZVMI=", + "pom": "sha256-5O1sZpYgNm+ZOSBln+CsfLyD11PbwNwOseUplzr5byM=" + }, + "com/gradleup/shadow#com.gradleup.shadow.gradle.plugin/9.2.2": { + "pom": "sha256-ZLCuyyPFfukfzPJXx4F8uyxpXQT565nM+9pth/TFlOk=" + }, + "com/gradleup/shadow#shadow-gradle-plugin/9.2.2": { + "jar": "sha256-rqYDnab2KTcMEKFxcOjz2o1nPhS++FgL0aZc3kSQc9A=", + "module": "sha256-4tXqtRULxjBI6WcI6t6/0XbmOfIsgFFoBVszcQdo3YI=", + "pom": "sha256-bO3IWZV0n5H/XNb8Z3H6PfZ4qlZSIl5uoFjNnXmVjGo=" + }, + "com/squareup/okhttp3#okhttp/4.12.0": { + "jar": "sha256-sQUAgbFLt6On5VpNPvAbXc+rxFO0VzpPwBl2cZHV9OA=", + "module": "sha256-YH4iD/ghW5Kdgpu/VPMyiU8UWbTXlZea6vy8wc6lTPM=", + "pom": "sha256-fHNwQKlBlSLnxQzAJ0FqcP58dinlKyGZNa3mtBGcfTg=" + }, + "com/squareup/okio#okio-jvm/3.6.0": { + "jar": "sha256-Z1Q/Bzb8QirpJ+0OUEuYvF4mn9oNNQBXkzfLcT2ihBI=", + "module": "sha256-scIZnhwMyWnvYcu+SvLsr5sGQRvd4By69vyRNN/gToo=", + "pom": "sha256-YbTXxRWgiU/62SX9cFJiDBQlqGQz/TURO1+rDeiQpX8=" + }, + "com/squareup/okio#okio/3.6.0": { + "module": "sha256-akesUDZOZZhFlAH7hvm2z832N7mzowRbHMM8v0xAghg=", + "pom": "sha256-rrO3CiTBA+0MVFQfNfXFEdJ85gyuN2pZbX1lNpf4zJU=" + }, + "commons-codec#commons-codec/1.16.0": { + "jar": "sha256-VllfsgsLhbyR0NUD2tULt/G5r8Du1d/6bLslkpAASE0=", + "pom": "sha256-bLWVeBnfOTlW/TEaOgw/XuwevEm6Wy0J8/ROYWf6PnQ=" + }, + "commons-io#commons-io/2.20.0": { + "jar": "sha256-35C7oP48tYa38WTnj+j49No/LdXCf6ZF+IgQDMwl3XI=", + "pom": "sha256-vb34EHLBkO6aixgaXFj1vZF6dQ+xOiVt679T9dvTOio=" + }, + "dev/equo/ide#solstice/1.7.5": { + "jar": "sha256-BuFLxDrMMx2ra16iAfxnNk7RI/mCyF+lEx8IF+1lrk8=", + "module": "sha256-eYp7cGdyE27iijLt2GOx6fgWE6NJhAXXS+ilyb6/9U8=", + "pom": "sha256-20U7urXn2opDE5sNzTuuZykzIfKcTZH1p5XZ/2xS3d8=" + }, + "io/github/gradle-nexus#publish-plugin/2.0.0": { + "jar": "sha256-lCwaFtFh9kYxkBtOLa1UHS/L/lHPAyOVXavgLiqe8qo=", + "module": "sha256-4T/01uEPKDtihxA8mC8Ha9YZ4qRh+znBbUTR0V1x6Pc=", + "pom": "sha256-V4e4+lvBAqYRbTWnztW7vPEZ/XJgQxs3kXPuNQU5rQk=" + }, + "io/github/gradle-nexus/publish-plugin#io.github.gradle-nexus.publish-plugin.gradle.plugin/2.0.0": { + "pom": "sha256-oymrlfS/3VyJEIPK06uzB0H9xroLspsRUqgP4KadYu8=" + }, + "jakarta/platform#jakarta.jakartaee-bom/9.1.0": { + "pom": "sha256-35jgJmIZ/buCVigm15o6IHdqi6Aqp4fw8HZaU4ZUyKQ=" + }, + "jakarta/platform#jakartaee-api-parent/9.1.0": { + "pom": "sha256-p3AsSHAmgCeEtXl7YjMKi41lkr8PRzeyXGel6sgmWcA=" + }, + "org/apache#apache/29": { + "pom": "sha256-PkkDcXSCC70N9jQgqXclWIY5iVTCoGKR+mH3J6w1s3c=" + }, + "org/apache#apache/35": { + "pom": "sha256-6il9zRFBNui46LYwIw1Sp2wvxp9sXbJdZysYVwAHKLg=" + }, + "org/apache/ant#ant-launcher/1.10.15": { + "jar": "sha256-XIVRmQMHoDIzbZjdrtVJo5ponwfU1Ma5UGAb8is9ahs=", + "pom": "sha256-ea+EKil53F/gAivAc8SYgQ7q2DvGKD7t803E3+MNrJU=" + }, + "org/apache/ant#ant-parent/1.10.15": { + "pom": "sha256-SYhPGHPFEHzCN/QoXER3R5uwgEvwc3OUgBsI114rvrA=" + }, + "org/apache/ant#ant/1.10.15": { + "jar": "sha256-djrNpKaViMnqiBepUoUf8ML8S/+h0IHCVl3EB/KdV5Q=", + "pom": "sha256-R4DmHoeBbu4fIdGE7Jl7Zfk9tfS5BCwXitsp4j50JdY=" + }, + "org/apache/commons#commons-parent/58": { + "pom": "sha256-LUsS4YiZBjq9fHUni1+pejcp2Ah4zuy2pA2UbpwNVZA=" + }, + "org/apache/commons#commons-parent/85": { + "pom": "sha256-0Yn/LAAn6Wu2XTHm8iftKvlmFps2rx6XPdW6CJJtx7U=" + }, + "org/apache/groovy#groovy-bom/4.0.27": { + "module": "sha256-1sIlTINHuEzahMr3SRShh8Lzd+QoTo2Ls/kBUhgQqos=", + "pom": "sha256-qkTrUr/f5h0ns+RQ0rNI2I3qo0N6tNnUmoQJU0j59vs=" + }, + "org/apache/logging/log4j#log4j-api/2.25.2": { + "jar": "sha256-n9Zsn+C+oG+pZmwUeYmkbK+qkrSoh1NpfTlFzEMzjLs=", + "module": "sha256-WPeF66u6zDA/Ow5aSF91X9qzKQ9p5JsDT4lj0ngjZV4=", + "pom": "sha256-CVYJaiUCQIyVioMXTytqV9yy5bB7uRTISHMrRLirvcM=" + }, + "org/apache/logging/log4j#log4j-bom/2.25.2": { + "pom": "sha256-Tym32cLZcP0qZpcXa/fd3EFQifYNaW0ov98xsk6S8Rw=" + }, + "org/apache/logging/log4j#log4j-core/2.25.2": { + "jar": "sha256-5Q23cBQw/5B5gYUO9SekHVGlPdABf1Oghgr8urhXAnc=", + "module": "sha256-JRQSc3eFDwR83jJbc7efriEzKSK+tkmiUzr9CEIlihE=", + "pom": "sha256-L/9GPTmclAgtmCLCG/v0cOEFHbt9S0XyWw54G8Xg9BI=" + }, + "org/apache/logging/log4j#log4j/2.25.2": { + "pom": "sha256-HYBXBY0LBcj3clyhrbpoc5y+rHWJjsoGpIymEVRsA+w=" + }, + "org/apache/maven#maven-api-annotations/4.0.0-rc-3": { + "jar": "sha256-XTSQ9yrTp+gr6IsnYp83xZ/SUxuuURw7E4ZkINXYYr0=", + "pom": "sha256-83HUqkRgxMwP4x0W20WC2+eGHvzS5nqvGEPimR8Xx0I=" + }, + "org/apache/maven#maven-api-xml/4.0.0-rc-3": { + "jar": "sha256-8+OzZCNzxp1MdEHUDroHZeHXROmStiGURS9epUUd/bo=", + "pom": "sha256-XxSOOelo08K3a4426hN3mJ8KeetDpqWa5yPZElzLXGE=" + }, + "org/apache/maven#maven-xml/4.0.0-rc-3": { + "jar": "sha256-BjxCTLR/dRZBJdXuolFnuTHdaU40Jo1QJHN050IR3Rk=", + "pom": "sha256-nZZekiyqwDYkl9J7v6UaRI+UydcTYjZnnGhSNwb3KYI=" + }, + "org/codehaus/plexus#plexus-utils/4.0.2": { + "jar": "sha256-iVcnTnX+LCeLFCjdFqDa7uHdOBUstu/4Fhd6wo/Mtpc=", + "pom": "sha256-UVHBO918w6VWlYOn9CZzkvAT/9MRXquNtfht5CCjZq8=" + }, + "org/codehaus/plexus#plexus-xml/4.1.0": { + "jar": "sha256-huan8HSE6LH3r2bZfTujyz1pKlRhtLHQordnDPV0jok=", + "pom": "sha256-uKO6h7WsMXVJUEngIXiIDKJczJ6rGkR9OKGbU3xXgk4=" + }, + "org/codehaus/plexus#plexus/20": { + "pom": "sha256-p7WUsAL8eRczyOlEcNCQRfT9aak61cN1dS8gV/hGM7Q=" + }, + "org/codehaus/woodstox#stax2-api/4.2.2": { + "jar": "sha256-phxI1VPvrXi8Af/8SsUovruuZMuuwXCypeOc9h61Gr4=", + "pom": "sha256-TpAuxVb8ZZi0HClS7BVz7cgVA35zMOxJIuq2GUImhuI=" + }, + "org/eclipse/ee4j#project/1.0.7": { + "pom": "sha256-IFwDmkLLrjVW776wSkg+s6PPlVC9db+EJg3I8oIY8QU=" + }, + "org/eclipse/jgit#org.eclipse.jgit-parent/6.7.0.202309050840-r": { + "pom": "sha256-u56FQW2Y0HMfx2f41w6EaAQWAdZnKuItsqx5n3qjkR8=" + }, + "org/eclipse/jgit#org.eclipse.jgit/6.7.0.202309050840-r": { + "jar": "sha256-tWRHfQkiQaqrUMhKxd0aw3XAGCBE1+VlnTpgqQ4ugBo=", + "pom": "sha256-BNB83b8ZjfpuRIuan7lA94HAEq2T2eqCBv4KTTplwZI=" + }, + "org/eclipse/platform#org.eclipse.osgi/3.18.300": { + "jar": "sha256-urlD5Y7dFzCSOGctunpFrsni2svd24GKjPF3I+oT+iI=", + "pom": "sha256-4nl2N1mZxUJ/y8//PzvCD77a+tiqRRArN59cL5fI/rQ=" + }, + "org/gradle/kotlin#gradle-kotlin-dsl-plugins/6.5.2": { + "jar": "sha256-O/9KBwDhyBXRlEifB7ugbLGQ6PKbdz03z+43rI1cdkQ=", + "module": "sha256-MVnFQXhFqWmTx4nULexDVwf7uEiXnP0R0LgzBZ5RIKM=", + "pom": "sha256-ctVO1m6jP6+UKCNRwxAZ4S59fpIOpnpRbL4ODWdBA0M=" + }, + "org/gradle/kotlin/kotlin-dsl#org.gradle.kotlin.kotlin-dsl.gradle.plugin/6.5.2": { + "pom": "sha256-5aavF7WFYNdGyrQseV/jpCoevkBQ5VpPANjPVM8x8eo=" + }, + "org/gradle/toolchains#foojay-resolver/1.0.0": { + "jar": "sha256-eLhqR9/fdpfJvRXaeJg/2A2nJH1uAvwQa98H4DiLYKg=", + "module": "sha256-YZDPDkLmZMEeGsCnhWmasCtUnOo0OSxnnzbYosVQ/Lk=", + "pom": "sha256-m8SLSeQi2e2rw5asGNiwQd/CIhLX+ujjVmfShdSBApo=" + }, + "org/gradle/toolchains/foojay-resolver-convention#org.gradle.toolchains.foojay-resolver-convention.gradle.plugin/1.0.0": { + "pom": "sha256-8TMkmhh1Suah0nAdANhJsa+6ewaD3bX8GxinAHHOwvo=" + }, + "org/jdom#jdom2/2.0.6.1": { + "jar": "sha256-CyD0XjoP2PDRLNxTFrBndukCsTZdsAEYh2+RdcYPMCw=", + "pom": "sha256-VXleEBi4rmR7k3lnz4EKmbCFgsI3TnhzwShzTIyRS/M=" + }, + "org/jetbrains#annotations/13.0": { + "jar": "sha256-rOKhDcji1f00kl7KwD5JiLLA+FFlDJS4zvSbob0RFHg=", + "pom": "sha256-llrrK+3/NpgZvd4b96CzuJuCR91pyIuGN112Fju4w5c=" + }, + "org/jetbrains/kotlin#abi-tools-api/2.2.20": { + "jar": "sha256-8chm6sXcCI8/0IEQCENAm/TxYSu7mY+6ofaFMlyuDVU=", + "pom": "sha256-HOL7NczYP8vJCuXFmN/bsilS0IVHgElEpbIfLEbKwL0=" + }, + "org/jetbrains/kotlin#fus-statistics-gradle-plugin/2.2.20": { + "module": "sha256-n+aVWzf+KQtlFiXPnGgea6IKjxLEZYzOUCblk1BaMSk=", + "pom": "sha256-P6gmRiy9hG0YAgLyLFGTQYy5zan2lcmUEjWpsbXBQdk=" + }, + "org/jetbrains/kotlin#fus-statistics-gradle-plugin/2.2.20/gradle813": { + "jar": "sha256-OmlZW2x1+/ktMgFodFxpj/cRS4YHhBBQ1ISYgjAG6HE=" + }, + "org/jetbrains/kotlin#kotlin-build-statistics/2.2.20": { + "jar": "sha256-+2VKT1vFY2h1kXMSnfSRB60J3FtBcrAXda+z+nJXndU=", + "pom": "sha256-tdU2T1fSH/FBgiBei2lf1oZNnYqleApu3EIJWEBHwRU=" + }, + "org/jetbrains/kotlin#kotlin-build-tools-api/2.2.20": { + "jar": "sha256-/ZlHs6F2Iahvq9lTr4fzS9K7f4sm2uksHte+dHL0TDs=", + "pom": "sha256-AtR9SHfsMktJbZMTMkXNTTSLZSMDzyfvpj44ry+zzyo=" + }, + "org/jetbrains/kotlin#kotlin-compiler-runner/2.2.20": { + "jar": "sha256-+vloNPogBNeL2ORCD1go3j1CckJ9ZHR5gCTqbpz4XN0=", + "pom": "sha256-kbsVJI9OqUS2Mw8xA/HrVF0TvditSuxDe3R6WG57F6k=" + }, + "org/jetbrains/kotlin#kotlin-daemon-client/2.2.20": { + "jar": "sha256-cO983NwwEHe5s7ohqp6cVadq+z/73+9KtWKmd9GN+kw=", + "pom": "sha256-ihNtDxPrmDpr40/x4WPJznmFXkuiF09Fy0KqpnVT91Q=" + }, + "org/jetbrains/kotlin#kotlin-gradle-plugin-annotations/2.2.20": { + "jar": "sha256-T8MqG+ZFynQE4hskRSCI+T6OmT6v/Sbza9Ndv3XGB1I=", + "pom": "sha256-sbbgEXktfKkv7K+/+sSlCPdvA5yfeuijI9GJKIgl9P4=" + }, + "org/jetbrains/kotlin#kotlin-gradle-plugin-api/2.2.20": { + "jar": "sha256-dgfuXHoMpT+lku58VA7oCJYqe62P7p7Xj+Z0hBRj2V0=", + "module": "sha256-T8vx/H5Uzr/pC5peD7RpYv7Vwi03I52iNfXi37xtUog=", + "pom": "sha256-C5E9oNIYhCAmOpBLtApkD9s1pTWnLwWC/llkHjoMSi4=" + }, + "org/jetbrains/kotlin#kotlin-gradle-plugin-api/2.2.20/gradle813": { + "jar": "sha256-dgfuXHoMpT+lku58VA7oCJYqe62P7p7Xj+Z0hBRj2V0=" + }, + "org/jetbrains/kotlin#kotlin-gradle-plugin-idea-proto/2.2.20": { + "jar": "sha256-dtFu5ZzeHmpwVWtdQEhu+fEcFkOodJPBnE3zMWU4N9k=", + "pom": "sha256-xRuhScfyk1nSWk7RIS4otpNOGkdW9VLAAHvxFE0onB0=" + }, + "org/jetbrains/kotlin#kotlin-gradle-plugin-idea/2.2.20": { + "jar": "sha256-7JacXwsJn4I4RiMiOPm9ZPPTdB5i6pBQrS5DL6150KA=", + "module": "sha256-/IW7KUlsw/X5DHjHonejkw7xFg8IQ/iu1ke3TGejtJQ=", + "pom": "sha256-NkQjJURfF7rCH1OGu0k4+D53K4NOWGBT1BRbGnXZ4oU=" + }, + "org/jetbrains/kotlin#kotlin-gradle-plugin-model/2.2.20": { + "jar": "sha256-U6MhUoJjIGAYUgSaC291OMqLtX/QnYeszRGLxo1D+OQ=", + "module": "sha256-EZdKVPSOCCXpdxML9u9qyZp/216yr53iZa9iTHY2g+U=", + "pom": "sha256-3uDjB7pub1GQPH5DPehSZ10eMOfyLPJGWxglVSZR7fs=" + }, + "org/jetbrains/kotlin#kotlin-gradle-plugin/2.2.20": { + "module": "sha256-3CS/pH4EQigykOIfBpoFYUHR8IjWy57Kouqs4bR7a4w=", + "pom": "sha256-ucP9Lr1UhNYMX+DbeqEIeDA+7d/JP5Qvc1wHupmBh8w=" + }, + "org/jetbrains/kotlin#kotlin-gradle-plugin/2.2.20/gradle813": { + "jar": "sha256-XTJbXCxdS8i/RBRdJOtNS+sGDRPRHr5IiYk27VzRVk4=" + }, + "org/jetbrains/kotlin#kotlin-gradle-plugins-bom/2.2.20": { + "module": "sha256-P7tFda43xKd2rrhtj/k8aqEbDPLadXScUyDiWFCwIp4=", + "pom": "sha256-PG1GnpFfuzCWrEy4wvRsedAnw8WQ5lihBoihVx61eNg=" + }, + "org/jetbrains/kotlin#kotlin-klib-commonizer-api/2.2.20": { + "jar": "sha256-OYK+RbEpMOIYGbWJ2zcHyOhM4le/Ks5/xi/I3zaPWz4=", + "pom": "sha256-CzAJtJQmv6F3qtlLSBCbjKVMck6i5sUGgmo6lc9ZEOE=" + }, + "org/jetbrains/kotlin#kotlin-metadata-jvm/2.2.20": { + "jar": "sha256-hSTqyQ9+jg8TZog/LGyCDJO/ph3z12hXyNPoA89nMV0=", + "pom": "sha256-e2qAtqLSZ2oEIvaWg4EyMVQlUfYbMgxochz7nh9ZCdA=" + }, + "org/jetbrains/kotlin#kotlin-native-utils/2.2.20": { + "jar": "sha256-UBd3SirqQf+HEhNxFs1NgAP+mroSAMEG5lcw/rW7dEI=", + "pom": "sha256-U+++4FpxIhiQYPXuXspodjnOr+KfXlmW3phiopxnJyU=" + }, + "org/jetbrains/kotlin#kotlin-serialization/2.2.20": { + "module": "sha256-iuU6MFsYxCBiyzt+qNRd0AvKEmDZfJZIr1SlaHnhz9g=", + "pom": "sha256-pP7tPG9RvldyFRaRsNKW4AyPESX3s3jg8TvQ43Te81k=" + }, + "org/jetbrains/kotlin#kotlin-serialization/2.2.20/gradle813": { + "jar": "sha256-dBE+hnGOJqOlpa8177uXaa2lBcVarELp14xoYEC3vUY=" + }, + "org/jetbrains/kotlin#kotlin-stdlib-common/2.3.0": { + "module": "sha256-/pAljbcTNVWBoBZDfQbAKdBpwgu93uE02R5LiHBz108=", + "pom": "sha256-J+2DQuBLgcqy0aP512D06/lQmG9n7Eme1y1cw4d+6NA=" + }, + "org/jetbrains/kotlin#kotlin-stdlib-jdk7/1.8.0": { + "pom": "sha256-36lkSmrluJjuR1ux9X6DC6H3cK7mycFfgRKqOBGAGEo=" + }, + "org/jetbrains/kotlin#kotlin-stdlib-jdk7/1.8.21": { + "pom": "sha256-m7EH1dXjkwvFl38AekPNILfSTZGxweUo6m7g8kjxTTY=" + }, + "org/jetbrains/kotlin#kotlin-stdlib-jdk7/1.9.10": { + "jar": "sha256-rGNhv5rR7TgsIQPZcSxHzewWYjK0kD7VluiHawaBybc=", + "pom": "sha256-x/pnx5YTILidhaPKWaLhjCxlhQhFWV3K5LRq9pRe3NU=" + }, + "org/jetbrains/kotlin#kotlin-stdlib-jdk8/1.8.21": { + "pom": "sha256-ODnXKNfDCaXDaLAnC0S08ceHj/XKXTKpogT6o0kUWdg=" + }, + "org/jetbrains/kotlin#kotlin-stdlib-jdk8/1.9.10": { + "jar": "sha256-pMdNlNZM4avlN2D+A4ndlB9vxVjQ2rNeR8CFoR7IDyg=", + "pom": "sha256-X0uU3TBlp3ZMN/oV3irW2B9A1Z+Msz8X0YHGOE+3py4=" + }, + "org/jetbrains/kotlin#kotlin-stdlib/2.3.0": { + "jar": "sha256-iHWHyRcTJQrVL+FK2RZtBCwzg1BJiQ6UN/NV/8WhlbE=", + "module": "sha256-CRCoo7aWD8eSxFxWqR18Oj8mKG8DKVVUtRnP83h1baI=", + "pom": "sha256-TVJW0+SETmVrDKQF9jUNbyF5XCQ3WzRSUmxUZ92ZtaI=" + }, + "org/jetbrains/kotlin#kotlin-tooling-core/2.2.20": { + "jar": "sha256-dAFOxPPveM59p+Pmlk8sUmoxIdXFj++MopeeXzRFgvQ=", + "pom": "sha256-jvep2QYs59w/xlVxXdAoqZRLeElhPgEYR8XWs7mSgXE=" + }, + "org/jetbrains/kotlin#kotlin-util-io/2.2.20": { + "jar": "sha256-1DGva+puLcmInE/iawc84VfxEchgj+laGL/gi4F8/3Q=", + "pom": "sha256-xqXQGEjNBAz8j3uuYjLXktcFwpOi2nJmrmJszbNdagM=" + }, + "org/jetbrains/kotlin#kotlin-util-klib-metadata/2.2.20": { + "jar": "sha256-vuSQHKU6WiHA22RZAdKwcK/2gkAkF91XiODjWTZFcTs=", + "pom": "sha256-vtAGUSIGX65328DEb/xBRqaFy7GLijApq9XaO/qhECc=" + }, + "org/jetbrains/kotlin#kotlin-util-klib/2.2.20": { + "jar": "sha256-7XyAlrK75HetF8MXjeuoyDr1MourNr/iEJEL1bQZI0w=", + "pom": "sha256-2mwiR3qvQt2hbYWa2unj7Yq8khzLp/9RYTTMi9NZqpI=" + }, + "org/jetbrains/kotlin/jvm#org.jetbrains.kotlin.jvm.gradle.plugin/2.2.20": { + "pom": "sha256-Trh97+w+fC/s4OGh7N8ipwzdYYmCYlVbHZLnx7PzuV0=" + }, + "org/jetbrains/kotlin/plugin/serialization#org.jetbrains.kotlin.plugin.serialization.gradle.plugin/2.2.20": { + "pom": "sha256-gKoyphE31QXES7HhNdkolZOm+UTOyAg6tZXAcd66xdg=" + }, + "org/jetbrains/kotlinx#kotlinx-coroutines-bom/1.8.0": { + "pom": "sha256-Ejnp2+E5fNWXE0KVayURvDrOe2QYQuQ3KgiNz6i5rVU=" + }, + "org/jetbrains/kotlinx#kotlinx-coroutines-core-jvm/1.8.0": { + "jar": "sha256-mGCQahk3SQv187BtLw4Q70UeZblbJp8i2vaKPR9QZcU=", + "module": "sha256-/2oi2kAECTh1HbCuIRd+dlF9vxJqdnlvVCZye/dsEig=", + "pom": "sha256-pWM6vVNGfOuRYi2B8umCCAh3FF4LduG3V4hxVDSIXQs=" + }, + "org/junit#junit-bom/5.11.4": { + "module": "sha256-qaTye+lOmbnVcBYtJGqA9obSd9XTGutUgQR89R2vRuQ=", + "pom": "sha256-GdS3R7IEgFMltjNFUylvmGViJ3pKwcteWTpeTE9eQRU=" + }, + "org/junit#junit-bom/5.13.1": { + "module": "sha256-M8B6uXJHkKblhZugfWkResUwQ5ckVFqBxBeeMnLHXeg=", + "pom": "sha256-+mhFHqgwVy7UP/5R11tqBfel5mWmAqUfSda+AgY6ZfM=" + }, + "org/junit#junit-bom/5.13.2": { + "module": "sha256-7WfhUiFASsQrXlmBAu33Yt1qlS3JUAHpwMTudKBOgoM=", + "pom": "sha256-Q7EQT7P9TvS3KpdR1B4Jwp8AHIvgD/OXIjjcFppzS0k=" + }, + "org/junit#junit-bom/5.9.3": { + "module": "sha256-tAH9JZAeWCpSSqU0PEs54ovFbiSWHBBpvytLv87ka5M=", + "pom": "sha256-TQMpzZ5y8kIOXKFXJMv+b/puX9KIg2FRYnEZD9w0Ltc=" + }, + "org/mockito#mockito-bom/4.11.0": { + "pom": "sha256-2FMadGyYj39o7V8YjN6pRQBq6pk+xd+eUk4NJ9YUkdo=" + }, + "org/ow2#ow2/1.5.1": { + "pom": "sha256-Mh3bt+5v5PU96mtM1tt0FU1r+kI5HB92OzYbn0hazwU=" + }, + "org/ow2/asm#asm-commons/9.8": { + "jar": "sha256-MwGhwctMWfzFKSZI2sHXxa7UwPBn376IhzuM3+d0BPQ=", + "pom": "sha256-95PnjwH3A3F9CUcuVs3yEv4piXDIguIRbo5Un7bRQMI=" + }, + "org/ow2/asm#asm-tree/9.8": { + "jar": "sha256-FLeIDLfIXu0QHicQQy/D/7gydVMqaolNxMQJXUmtWfE=", + "pom": "sha256-cUnn+qDhkSlvh5ru2SCciULTmPBpjSzKGpxijy4qj3c=" + }, + "org/ow2/asm#asm/9.8": { + "jar": "sha256-h26raoPa7K1cpn65/KuwY8l7WuuM8fynqYns3hdSIFE=", + "pom": "sha256-wTZ8O7OD12Gef3l+ON91E4hfLu8ErntZCPaCImV7W6o=" + }, + "org/slf4j#slf4j-api/1.7.36": { + "jar": "sha256-0+9XXj5JeWeNwBvx3M5RAhSTtNEft/G+itmCh3wWocA=", + "pom": "sha256-+wRqnCKUN5KLsRwtJ8i113PriiXmDL0lPZhSEN7cJoQ=" + }, + "org/slf4j#slf4j-parent/1.7.36": { + "pom": "sha256-uziNN/vN083mTDzt4hg4aTIY3EUfBAQMXfNgp47X6BI=" + }, + "org/sonatype/oss#oss-parent/5": { + "pom": "sha256-FnjUEgpYXYpjATGu7ExSTZKDmFg7fqthbufVqH9SDT0=" + }, + "org/sonatype/oss#oss-parent/7": { + "pom": "sha256-tR+IZ8kranIkmVV/w6H96ne9+e9XRyL+kM5DailVlFQ=" + }, + "org/springframework#spring-framework-bom/5.3.39": { + "module": "sha256-+ItA4qUDM7QLQvGB7uJyt17HXdhmbLFFvZCxW5fhg+M=", + "pom": "sha256-9tSBCT51dny6Gsfh2zj49pLL4+OHRGkzcada6yHGFIs=" + }, + "org/tukaani#xz/1.9": { + "jar": "sha256-IRswbPxE+Plt86Cj3a91uoxSie7XfWDXL4ibuFX1NeU=", + "pom": "sha256-CTvhsDMxvOKTLWglw36YJy12Ieap6fuTKJoAJRi43Vo=" + }, + "org/vafer#jdependency/2.13": { + "jar": "sha256-FFxghksjansSllUNMaSap1rWEpWBOO4NRxufywu+3T4=", + "pom": "sha256-3Ip1HRudXz2imiihDmKF62+3Q/TW46MleRsZX9B4eXs=" + } + }, + "https://repo.maven.apache.org/maven2": { + "com/github/ajalt/clikt#clikt-core-jvm/5.0.1": { + "jar": "sha256-6MQ3DbBsafpIBpg7DAqG66kcGl+t223zFVeP2PO3Nbc=", + "module": "sha256-sYgdfY/3L7JojLSINHD7yoS8+IEMtp7DBQtJ2+HKWX8=", + "pom": "sha256-OtciKGhLlaLbN2X+aq3UYIRg7xMAFtKUjkcza76KweQ=" + }, + "com/github/ajalt/clikt#clikt-core/5.0.1": { + "jar": "sha256-JwglyC7ZsrjrYc6JB7DDdSyxcs8uWdBq1qauT/I2Y4I=", + "module": "sha256-KjDYyQXrcpOZYb6XLFQC/zH+HaM6sUcFUE9ZW/HFSFI=", + "pom": "sha256-TF2rhycC1WIleXiYJWIXQkzXhmtaRTj5dJ2U5iWh0Qk=" + }, + "com/github/ajalt/clikt#clikt-jvm/5.0.1": { + "jar": "sha256-7Dh/4hiEqIC8iH3RRNkoqVww0zaX6IDbyPSk4wWjX7c=", + "module": "sha256-gUIIpzSmW/HUSSaQ+6HXo/sZkscto5+XVYvzzaOM9yQ=", + "pom": "sha256-x/MSuihScDCMhWXTzqcYtmiWscDwbgpml8PlJPjNocI=" + }, + "com/github/ajalt/clikt#clikt/5.0.1": { + "jar": "sha256-UdVCOnMQs71H3+hw1kvQzJIgMMouDCwglDnaHtUIHo4=", + "module": "sha256-qnl8TNixzzOsXias5ZINL6z3JyxVgjzhqyPiwmecpi0=", + "pom": "sha256-32VyyN7wxHjO/GsaQyQRQ134AfKj66rzt1zZ+YruggY=" + }, + "com/github/ajalt/colormath#colormath-jvm/3.6.0": { + "jar": "sha256-WfdBrf5iBTBmeC2LGkWv0GaFpLxkszJ35Uh2uZPtiFw=", + "module": "sha256-P6dnMPmJ4ChN8YL87IViDZtIrjIhOYhBrGyviEYvYvg=", + "pom": "sha256-8Dw11QURDQZzNF9HQOVbzZdqmp+lobE8qirTmPO8Hl0=" + }, + "com/github/ajalt/colormath#colormath/3.6.0": { + "jar": "sha256-49ox0EqJXlNfXQh2TM9fODQcQr99aNqW6h8ACfclmdY=", + "module": "sha256-aQeqSXrbmvY4EsdTZjic7T5ruL7oDnsjmttMU2c/iIQ=", + "pom": "sha256-zh3tjA259LxNNjS64Vn9jVu2qWDyzTuWoAyPDnnOZAs=" + }, + "com/github/ajalt/mordant#mordant-core-jvm/3.0.0": { + "jar": "sha256-nPm0bR9J8tbPJjVGKyncWeDCmx+y8IWzMSiIu+nHzTE=", + "module": "sha256-I+BlmP75Kdgc3+0wpYQkP0d148M3HgkAGtJ7q3iMtxk=", + "pom": "sha256-r3v/7gGvI42tX1e2vq/5yZkAGVUekukB3MGp5sO7FFM=" + }, + "com/github/ajalt/mordant#mordant-core/3.0.0": { + "jar": "sha256-c/UXnY6U+FEUR18Zlo0WWURZTmszjbcciwv9sJUe6z4=", + "module": "sha256-Mp8Z5l18bMLwlOudZHrcisUR2MDqxMQYtM092KK8gEg=", + "pom": "sha256-8wg4H/t0oDTPnl76edME/JdTFEUo+JytLR9wozV8zXo=" + }, + "com/github/ajalt/mordant#mordant-jvm-ffm-jvm/3.0.0": { + "jar": "sha256-D9tgAq3XJ9rrrRl0348304xFpwRF6+SDUBc6gbHHI1A=", + "module": "sha256-oFAbyadCBH033AYFgEGH4e5vlzMC3br0dWKTnNBHjgc=", + "pom": "sha256-ecpb3mvuyOfFkKN+wJ4H6u+ezX5qYP8dzwijIqBynCQ=" + }, + "com/github/ajalt/mordant#mordant-jvm-ffm/3.0.0": { + "module": "sha256-bPgGG6IsR85tA81oU0028Lq4OFx7tlSMEPSpfdvv+hg=", + "pom": "sha256-yOHJMW4ZPBDh848i4efXbDKTJTmJUODhEAXPwPMHg54=" + }, + "com/github/ajalt/mordant#mordant-jvm-graal-ffi-jvm/3.0.0": { + "jar": "sha256-nCYGjFf6my3swkIGcZ0uXDDQ/zjy+ndPMuT5LrBWW1A=", + "module": "sha256-atsKs7W+MCvGGIgrUergISQ3aReYRgBDzl5+W2O6X4M=", + "pom": "sha256-uhw8nvIw7dshu5+stNVfFlLJsO/mXTFcCxKiim+F8uM=" + }, + "com/github/ajalt/mordant#mordant-jvm-graal-ffi/3.0.0": { + "module": "sha256-DdOCuSfkCzk8n6Ft4ZDhbshQ+IJY4ik8JKhcp79HoO0=", + "pom": "sha256-2CS/RFvlkqVPD9LE1HVPYUz27NnkdDDM0sORIJb7nJA=" + }, + "com/github/ajalt/mordant#mordant-jvm-jna-jvm/3.0.0": { + "jar": "sha256-5frjBHFLjFortS3ud4PGAbG/9vwgRzjiftQw04W5bew=", + "module": "sha256-UtzaqyOHm9BKVjKJTx2Nnzher5ImW30DJzXUCAdOcAE=", + "pom": "sha256-CmdkL+KE5LGzqcfa8th/vC6zSQ6TIYMImjlOb006fuE=" + }, + "com/github/ajalt/mordant#mordant-jvm-jna/3.0.0": { + "module": "sha256-uGPRWlZXmXVu0MRr2KYFL4/KXEhgnoGSKWcWchXaBQA=", + "pom": "sha256-tAleJ06OTAjMKGLo9XItnb4hIUOvStGQcsPJ9H/eVU4=" + }, + "com/github/ajalt/mordant#mordant-jvm/3.0.0": { + "jar": "sha256-ntO5dvzMx42nRtSYZvqOu48QUwqTxUTqBCAlmmB92V4=", + "module": "sha256-n1EkiBM1KEwAVFcxz1lweTz/oF5JU9kM2Sqahl4ZeG0=", + "pom": "sha256-L9Kv6Kzg57uKrloGzP5I6JopxUABAhT8/8GkWRWgPkY=" + }, + "com/github/ajalt/mordant#mordant/3.0.0": { + "jar": "sha256-CQmE0gJpL/70R+iN/ixjaTpd4pZw2ggxuGO8KE2hR+I=", + "module": "sha256-QIja+Do8Ni/a4lmFgvqGly+pe1xPAogm7RvO+kLkVNM=", + "pom": "sha256-teBT1txYME807CTlzJdbnIqo1c/o1HQ8raE8mpVgFXg=" + }, + "com/github/ben-manes/caffeine#caffeine/2.9.3": { + "jar": "sha256-Hgp7vvHdeRZTFD8/BdDkiZNL9UgeWKh8nmGc1Gtocps=", + "module": "sha256-J9/TStZlaZDTxzF2NEsJkfLIJwn6swcJs93qj6MAMHA=", + "pom": "sha256-b6TxwQGSgG+O8FtdS+e9n1zli4dvZDZNTpDD/AkjI9w=" + }, + "com/google/code/gson#gson-parent/2.11.0": { + "pom": "sha256-issfO3Km8CaRasBzW62aqwKT1Sftt7NlMn3vE6k2e3o=" + }, + "com/google/code/gson#gson-parent/2.13.2": { + "pom": "sha256-g6tSip1Q/XauuK1vcns+6ct2ZYYlV3TtFsqMTHbZ2s0=" + }, + "com/google/code/gson#gson/2.11.0": { + "jar": "sha256-V5KNblpu3rKr03cKj5W6RNzkXzsjt6ncKzCcWBVSp4s=", + "pom": "sha256-wOVHvqmYiI5uJcWIapDnYicryItSdTQ90sBd7Wyi42A=" + }, + "com/google/code/gson#gson/2.13.2": { + "jar": "sha256-3QzhtVo+0ggMtw+cZVhQzahsIGhiMQAJ3LXlyVJlpeA=", + "pom": "sha256-OqBqp8D5rwkpYaQtCVeOQyS+FGNIoO5u1HhX98Jne3Y=" + }, + "com/google/code/gson/gson/maven-metadata": { + "xml": { + "groupId": "com.google.code.gson", + "lastUpdated": "20250910210152", + "release": "2.13.2" + } + }, + "com/google/errorprone#error_prone_annotations/2.27.0": { + "jar": "sha256-JMkjNyxY410LnxagKJKbua7cd1IYZ8J08r0HNd9bofU=", + "pom": "sha256-TKWjXWEjXhZUmsNG0eNFUc3w/ifoSqV+A8vrJV6k5do=" + }, + "com/google/errorprone#error_prone_annotations/2.28.0": { + "jar": "sha256-8/yKOgpAIHBqNzsA5/V8JRLdJtH4PSjH04do+GgrIx4=", + "pom": "sha256-DOkJ8TpWgUhHbl7iAPOA+Yx1ugiXGq8V2ylet3WY7zo=" + }, + "com/google/errorprone#error_prone_annotations/2.41.0": { + "jar": "sha256-pW54K1tQgRrCBAc6NVoh2RWiEH/OE+xxEzGtA29mD8w=", + "pom": "sha256-oVHfHi4LSGGNiwahgHSKKbOrs5sbI5b2och5pydIjG4=" + }, + "com/google/errorprone#error_prone_parent/2.27.0": { + "pom": "sha256-+oGCnQSVWd9pJ/nJpv1rvQn4tQ5tRzaucsgwC2w9dlQ=" + }, + "com/google/errorprone#error_prone_parent/2.28.0": { + "pom": "sha256-rM79u1QWzvX80t3DfbTx/LNKIZPMGlXf5ZcKExs+doM=" + }, + "com/google/errorprone#error_prone_parent/2.41.0": { + "pom": "sha256-xTg4jXYKXByY3PBvbtPP5fEaZRgn21y9LtgojHlcrUI=" + }, + "de/undercouch#gradle-download-task/5.6.0": { + "jar": "sha256-zkN6arnKcZzIVrVbp0kuQsTODumC5tIvtDLNVYh2gb4=", + "module": "sha256-P+YJN66Dzs2qpOD2EykVaQKD7d+IQ54m8efjgEV4NSI=", + "pom": "sha256-RqMBkMaLY9AegKQEQJfCULu8MgmkXw3FpNDioe1bgKc=" + }, + "io/github/java-diff-utils#java-diff-utils-parent/4.12": { + "pom": "sha256-2BHPnxGMwsrRMMlCetVcF01MCm8aAKwa4cm8vsXESxk=" + }, + "io/github/java-diff-utils#java-diff-utils/4.12": { + "jar": "sha256-mZCiA5d49rTMlHkBQcKGiGTqzuBiDGxFlFESGpAc1bU=", + "pom": "sha256-wm4JftyOxoBdExmBfSPU5JbMEBXMVdxSAhEtj2qRZfw=" + }, + "io/github/tree-sitter#jtreesitter/0.25.0": { + "jar": "sha256-VvmvL24sKdmWKmmwW1kvDiTM7CKXkuDG8x+luNpsuas=", + "pom": "sha256-P6HmvNefYkFzBc5E5T2NEWS/c7UWDg46mjQtkuPAjaM=" + }, + "io/opentelemetry#opentelemetry-api/1.41.0": { + "jar": "sha256-kzZmjziN5ooKLD4RQVT+vSnbGadkTyfE66VI9d6FIlg=", + "module": "sha256-JhRoWJyZO4j3zK2TOP/YFmud9Mw03jCAZpMrKlyd6VY=", + "pom": "sha256-vB7w93s71tsLem8WUv20IWmKDlQ3RQSnQZ+xvrRWKZM=" + }, + "io/opentelemetry#opentelemetry-context/1.41.0": { + "jar": "sha256-XjQypEZKQyq/2rc75xQuUW0lqEqoQm/OEZL/sFMvqjU=", + "module": "sha256-F03No+hAImE4rbjOkENXbdxDwoa2EtPb9kUx9nvY2Yc=", + "pom": "sha256-yzs5YlmYNClZTWLkdmdcwAuyUskNMU1keptW3sI+VSY=" + }, + "net/bytebuddy#byte-buddy-parent/1.14.18": { + "pom": "sha256-dpxdCxEV5z9oRSu6qjVrrZtzfV/ge4PUyeSDl1uB/jw=" + }, + "net/bytebuddy#byte-buddy/1.14.18": { + "jar": "sha256-UhF68WlqU6p3wTE1MHStolzL3y31EfKvM/rWcE+pUQQ=", + "pom": "sha256-IvsZB4/nBkirYFSSZscUaZaEciaqbm9+iP7MCBRySTk=" + }, + "net/java/dev/jna#jna/5.14.0": { + "jar": "sha256-NO0eHyf6iWvKUNvE6ZzzcylnzsOHp6DV40hsCWc/6MY=", + "pom": "sha256-4E4llRUB3yWtx7Hc22xTNzyUiXuE0+FJISknY+4Hrj0=" + }, + "org/apiguardian#apiguardian-api/1.1.2": { + "jar": "sha256-tQlEisUG1gcxnxglN/CzXXEAdYLsdBgyofER5bW3Czg=", + "module": "sha256-4IAoExN1s1fR0oc06aT7QhbahLJAZByz7358fWKCI/w=", + "pom": "sha256-MjVQgdEJCVw9XTdNWkO09MG3XVSemD71ByPidy5TAqA=" + }, + "org/assertj#assertj-core/3.26.3": { + "jar": "sha256-TC+GQY/0fua2f7xq2xlOgCGbeTKBs72ih5nUQlvJoL0=", + "pom": "sha256-fXXFEKu7fcuR+zXOLxQG6AAP+rnuGPqpqt8xXxlzgWY=" + }, + "org/bouncycastle#bcpg-jdk18on/1.80": { + "jar": "sha256-N5mg1TURuGB4CvQ8RK6OBRmvq7JjGVr0AXT0YxLNWL0=", + "pom": "sha256-B6JGwq2vtih4OhroSsGNTfQCXfBq42NN/pOHq5T56nQ=" + }, + "org/bouncycastle#bcpkix-jdk18on/1.80": { + "jar": "sha256-T0umqSYX6hncGD8PpdtJLu5Cb93ioKLWyUd3/9GvZBM=", + "pom": "sha256-pKEiETRntyjhjyb7DP1X8LGg18SlO4Zxis5wv4uG7Uc=" + }, + "org/bouncycastle#bcprov-jdk18on/1.80": { + "jar": "sha256-6K0gn4xY0pGjfKl1Dp6frGBZaVbJg+Sd2Cgjgd2LMkk=", + "pom": "sha256-oKdcdtkcQh7qVtD2Bi+49j7ff6x+xyT9QgzNytcYHUM=" + }, + "org/bouncycastle#bcutil-jdk18on/1.80": { + "jar": "sha256-Iuymh/eVVBH0Vq8z5uqOaPxzzYDLizKqX3qLGCfXxng=", + "pom": "sha256-Qhp95L/rnFs4sfxHxCagh9kIeJVdQQf1t6gusde3R7Y=" + }, + "org/bouncycastle/bcprov-jdk18on/maven-metadata": { + "xml": { + "groupId": "org.bouncycastle", + "lastUpdated": "20251126065922", + "release": "1.83" + } + }, + "org/bouncycastle/bcutil-jdk18on/maven-metadata": { + "xml": { + "groupId": "org.bouncycastle", + "lastUpdated": "20251126070121", + "release": "1.83" + } + }, + "org/checkerframework#checker-qual/3.43.0": { + "jar": "sha256-P7wumPBYVMPfFt+auqlVuRsVs+ysM2IyCO1kJGQO8PY=", + "module": "sha256-+BYzJyRauGJVMpSMcqkwVIzZfzTWw/6GD6auxaNNebQ=", + "pom": "sha256-kxO/U7Pv2KrKJm7qi5bjB5drZcCxZRDMbwIxn7rr7UM=" + }, + "org/eclipse/lsp4j#org.eclipse.lsp4j.jsonrpc/0.23.1": { + "jar": "sha256-ThqndHTeF5HZbcVZMvtG799TIzVI849iunN2+LC8ZlA=", + "pom": "sha256-sUKwxX5ehkAbfx1Zzm7ONhg2nQgIKYmk2s8fm8h775k=" + }, + "org/eclipse/lsp4j#org.eclipse.lsp4j/0.23.1": { + "jar": "sha256-sWu8YjKjlG4D1Te7m+dOGEidvGqLjFq2y3mAhU34RA8=", + "pom": "sha256-IwsIShj5HcfsxUV9rPzP3I/da/fERrzYiBXT8JAYY1E=" + }, + "org/jetbrains#annotations/13.0": { + "jar": "sha256-rOKhDcji1f00kl7KwD5JiLLA+FFlDJS4zvSbob0RFHg=", + "pom": "sha256-llrrK+3/NpgZvd4b96CzuJuCR91pyIuGN112Fju4w5c=" + }, + "org/jetbrains/kotlin#abi-tools-api/2.2.20": { + "jar": "sha256-8chm6sXcCI8/0IEQCENAm/TxYSu7mY+6ofaFMlyuDVU=", + "pom": "sha256-HOL7NczYP8vJCuXFmN/bsilS0IVHgElEpbIfLEbKwL0=" + }, + "org/jetbrains/kotlin#abi-tools-api/2.3.0": { + "jar": "sha256-QBe8wfXTKFsX5rYiDRakaMhxWTDw35Y5bXZLV/Cm02U=", + "pom": "sha256-qJIhRAlxhudUjXFH3I3O1eYOMGnUY1ulUe8uV3WrmAk=" + }, + "org/jetbrains/kotlin#abi-tools/2.2.20": { + "jar": "sha256-paY3q4IXBJkc9wYWtk4CuaT4IMggGfk8DSbwzfsiZjM=", + "pom": "sha256-ffGAKEU136pgq3jo5pNEiC5c3Np9clU1sRDgOu6MWFA=" + }, + "org/jetbrains/kotlin#fus-statistics-gradle-plugin/2.3.0": { + "module": "sha256-d8IDSG/XkrWAVyBp5cRC0YDA7wVbpzRQXaKNGX7BL/c=", + "pom": "sha256-k8/rFUWRw3AengQ1C9AF0ZVqmuZHFH1vsqfFeD4fkJo=" + }, + "org/jetbrains/kotlin#fus-statistics-gradle-plugin/2.3.0/gradle813": { + "jar": "sha256-D+cv3G5RvisnSw5kuEQB9SUDavsgYWKkRYIU8R6614c=" + }, + "org/jetbrains/kotlin#kotlin-assignment-compiler-plugin-embeddable/2.3.0": { + "jar": "sha256-1D+qszfzaY6NrOZSXSzXwq66ewNZOEhYZBZP5wLnM70=", + "pom": "sha256-UttpIUdRA18/LYUvJDCC5x566wenFs2eCHskuYJ6Uio=" + }, + "org/jetbrains/kotlin#kotlin-assignment/2.3.0": { + "module": "sha256-ljRPGdjxnqpbxEHjP2UB9+c9o8el1X1EvHwW6qRXVls=", + "pom": "sha256-GqwiGC0snJVP/8FrV2Xq0jjiAw9HhIctRrxfOcozll0=" + }, + "org/jetbrains/kotlin#kotlin-assignment/2.3.0/gradle813": { + "jar": "sha256-PKIayHkWchdbgnPemV8CTzWZLfAwCijCdUnPd3DMnOY=" + }, + "org/jetbrains/kotlin#kotlin-build-statistics/2.3.0": { + "jar": "sha256-Z3hVlwDnLXZOniTZWPFuwMx152t1s4FMK83hRKYnT04=", + "pom": "sha256-QTkKXrIxFJOxpFubOXLDoL8dqEfoRaNKtoQKr0EmTeI=" + }, + "org/jetbrains/kotlin#kotlin-build-tools-api/2.2.20": { + "jar": "sha256-/ZlHs6F2Iahvq9lTr4fzS9K7f4sm2uksHte+dHL0TDs=", + "pom": "sha256-AtR9SHfsMktJbZMTMkXNTTSLZSMDzyfvpj44ry+zzyo=" + }, + "org/jetbrains/kotlin#kotlin-build-tools-api/2.3.0": { + "jar": "sha256-xsCc8oU0VySfcHyGOCESQJ1aVfULa4Vouk9TDdAD/tw=", + "pom": "sha256-FzTIvg4nFXJ3AkW01gbOW7iBbosNHA9+euEcEMLKF1s=" + }, + "org/jetbrains/kotlin#kotlin-build-tools-compat/2.3.0": { + "jar": "sha256-AJST4crwTIKF8f7RV7uraINV2wTS3q7E9aD5tjX4ZuU=", + "pom": "sha256-ayMWTiKBY/1j+Bf7LF3ifW6cNAO3Y8y6BoBYTyF/jjI=" + }, + "org/jetbrains/kotlin#kotlin-build-tools-impl/2.2.20": { + "jar": "sha256-ZHiafwBWWSfy8/LRCfIwV009kwjtXW6Gv8qEPaZIfPc=", + "pom": "sha256-4vQ157rwHeL/kNCoc3r4+b+X/BUuWVuGp2C6ZOjmnfY=" + }, + "org/jetbrains/kotlin#kotlin-build-tools-impl/2.3.0": { + "jar": "sha256-k6Xo/7EACAHIMqhisjvZdm9ETm9sGFwysftXh3+1zqM=", + "pom": "sha256-AKelPNgr5ws234oCI6Wrhhoqb/txnZt3M3XId8idugQ=" + }, + "org/jetbrains/kotlin#kotlin-compiler-embeddable/2.2.20": { + "jar": "sha256-HGw/gQv+akGry8NLOi72OfHj9K7tOpU6Swl07qT0GIk=", + "pom": "sha256-Bf8CX3+wky+xH6HhzK71KPdgJ9lWaA+INdQ4VCdi4go=" + }, + "org/jetbrains/kotlin#kotlin-compiler-embeddable/2.3.0": { + "jar": "sha256-jb2IL6WMPRfmg6JzkCiDFfi0kPjj47G+TcPigNN+KFo=", + "pom": "sha256-zOmxEfIRZgxcRTk+dI30aVC2HAEUfgdzttxxnui7d6g=" + }, + "org/jetbrains/kotlin#kotlin-compiler-runner/2.2.20": { + "jar": "sha256-+vloNPogBNeL2ORCD1go3j1CckJ9ZHR5gCTqbpz4XN0=", + "pom": "sha256-kbsVJI9OqUS2Mw8xA/HrVF0TvditSuxDe3R6WG57F6k=" + }, + "org/jetbrains/kotlin#kotlin-compiler-runner/2.3.0": { + "jar": "sha256-hwl38pYFQ2xesiV7nI5dZPMoLyqI7e5FRNNOxF8Wpqc=", + "pom": "sha256-ov8zZnin9TpEOSv8bFK2gS7dxz5AghyQfyomQWeeDrs=" + }, + "org/jetbrains/kotlin#kotlin-daemon-client/2.2.20": { + "jar": "sha256-cO983NwwEHe5s7ohqp6cVadq+z/73+9KtWKmd9GN+kw=", + "pom": "sha256-ihNtDxPrmDpr40/x4WPJznmFXkuiF09Fy0KqpnVT91Q=" + }, + "org/jetbrains/kotlin#kotlin-daemon-client/2.3.0": { + "jar": "sha256-sr5naIyvEaE41a4M4SNTga2asN25OVqwb42EagtGYBc=", + "pom": "sha256-teLnTuXC+I/Qi/g+7GRx9rvKeqEhWYgCtsMsVuhwYCY=" + }, + "org/jetbrains/kotlin#kotlin-daemon-embeddable/2.2.20": { + "jar": "sha256-fFyM0vi+rdMoMjm7nKIZoMr6GvAmgrQHsYHhF5cY8vc=", + "pom": "sha256-9Yhmv7yYZ8bWR1ec/3DUKHeZctvd2N5MJXh5y0N0FIk=" + }, + "org/jetbrains/kotlin#kotlin-daemon-embeddable/2.3.0": { + "jar": "sha256-ObywLYwpOqZ4VUyLSdf/hGVwIXCSg8YYbjpAgGr5vRA=", + "pom": "sha256-TI3aucQLMNAbhc/qHxd31zUlybcWQ+YlDGV2/H0fwRI=" + }, + "org/jetbrains/kotlin#kotlin-gradle-plugin-annotations/2.3.0": { + "jar": "sha256-/HKYw2tF820WUscDuraT9f6bEVmOzZrH7J/f6IptsPA=", + "pom": "sha256-aQhDRFhvUkNNH7tRZmlgr/uHbGQ+gIYkXrgLpeREm7U=" + }, + "org/jetbrains/kotlin#kotlin-gradle-plugin-api/2.3.0": { + "module": "sha256-BQ8eECIJAOR6MIkd1c/qg3pl27sNmjyxuFKRq6MmykE=", + "pom": "sha256-GeTwq/tcvwEdJZP1ZRV8jr9FkvMAhhS6LtsEinhRF10=" + }, + "org/jetbrains/kotlin#kotlin-gradle-plugin-api/2.3.0/gradle813": { + "jar": "sha256-yh6tFJqMj0G6YKg5qDsjw6BiJ8RsYhJMUb6ZkDXerDE=" + }, + "org/jetbrains/kotlin#kotlin-gradle-plugin-idea-proto/2.3.0": { + "jar": "sha256-epBQPJ14f5vNLBd25MxSfrneeRLvecbJWPEwWi0cQ7o=", + "pom": "sha256-2bCOHuM4pjRhSanQIY+FHuPUfYu3fWEDJg8qhyauUl4=" + }, + "org/jetbrains/kotlin#kotlin-gradle-plugin-idea/2.3.0": { + "jar": "sha256-lS5zlI4qINOYwmuHprtwzPZkGPuvFSfDUVsYjqnUvWA=", + "module": "sha256-kRUvrqO8DJTbZtPLWKrh2+rXyGC1dk5nsgC01N9M6rk=", + "pom": "sha256-BLSVrgZj7a3aSEBkZKzTlDGjysez6OjAlLdplu1BSaA=" + }, + "org/jetbrains/kotlin#kotlin-gradle-plugin/2.3.0": { + "module": "sha256-vbMts6ongF80eewDPsY9PMq+sTuInYbavadCu+9TwJo=", + "pom": "sha256-h4cnvyGoOtrYnU5giEhN2/OfaoSpQoAiGm4cL4hBMD4=" + }, + "org/jetbrains/kotlin#kotlin-gradle-plugin/2.3.0/gradle813": { + "jar": "sha256-Os+X1zolgbAyhz+on1Z1DTazt21A1pLZg58kZ92uTWk=" + }, + "org/jetbrains/kotlin#kotlin-gradle-plugins-bom/2.3.0": { + "module": "sha256-/9anNzSypS+3Yz8WOVRL43HgYpxR2X+h5fteqTQ6Fwk=", + "pom": "sha256-cYYJKmnzaIQyxcCsuQtKZx/Y7oANhP5YT66P1TO6v4o=" + }, + "org/jetbrains/kotlin#kotlin-klib-commonizer-api/2.3.0": { + "jar": "sha256-m6V9kaveq5rNWIoUtfQiCbmWRMU0Ft/56X7LS/l/Ukg=", + "pom": "sha256-zTHw7NqCplEi/8alXehxE5S2GEtRRAK34RkZGqgJGoI=" + }, + "org/jetbrains/kotlin#kotlin-klib-commonizer-embeddable/2.2.20": { + "jar": "sha256-XeE7Sb8dRBCZFep1tjiWZpNpXfQM1bIebhKu62fcKSQ=", + "pom": "sha256-ZPn6nRljGcOOL66C6dzpm5q3nxo6SSv9BOoc0GxKhWY=" + }, + "org/jetbrains/kotlin#kotlin-metadata-jvm/2.2.20": { + "jar": "sha256-hSTqyQ9+jg8TZog/LGyCDJO/ph3z12hXyNPoA89nMV0=", + "pom": "sha256-e2qAtqLSZ2oEIvaWg4EyMVQlUfYbMgxochz7nh9ZCdA=" + }, + "org/jetbrains/kotlin#kotlin-native-utils/2.3.0": { + "jar": "sha256-7kvygz0Q5fN90haoMSn1nzx8vvXrrBMPcGZIOXCMgLc=", + "pom": "sha256-Rp6PYB/b34kP+ozXSOEQcCqkUxu7KbZOXnUD8O/lDZA=" + }, + "org/jetbrains/kotlin#kotlin-reflect/1.6.10": { + "jar": "sha256-MnesECrheq0QpVq+x1/1aWyNEJeQOWQ0tJbnUIeFQgM=", + "pom": "sha256-V5BVJCdKAK4CiqzMJyg/a8WSWpNKBGwcxdBsjuTW1ak=" + }, + "org/jetbrains/kotlin#kotlin-reflect/2.2.20": { + "jar": "sha256-ggkIOkp8TkdtmEKweDCPqWqW8Hpr2Z8F81hu4TKJqyY=", + "pom": "sha256-TidHQGbbg/uixZB0KJunEr6MhRV83guQUCmkRcJ19bo=" + }, + "org/jetbrains/kotlin#kotlin-reflect/2.3.0": { + "jar": "sha256-cU30voGVRf9N4fNqohg+DeqUtMjN98op6ciZGbrzY2I=", + "pom": "sha256-89F2jvL7UxBIPb6R4w6fo2x7Pi/e5pRaCLujEBQtKcE=" + }, + "org/jetbrains/kotlin#kotlin-sam-with-receiver-compiler-plugin-embeddable/2.3.0": { + "jar": "sha256-QOPhi7jFUjLuZSXOV9F7hl5WcosFK/DBDnvsHQhJmMo=", + "pom": "sha256-BnzIEOCaYeFufoe1Kk3WR+FfYLd+PaDRsVPz+A7r8wM=" + }, + "org/jetbrains/kotlin#kotlin-sam-with-receiver/2.3.0": { + "module": "sha256-DCD2gdNvSnmRYiFYCYkpF5TmVlgvlJwGed+kNKAm9so=", + "pom": "sha256-DILmEXpGNhvUzF5iZV8rgC8Q9LfZJWpjaD0DSv0MDUM=" + }, + "org/jetbrains/kotlin#kotlin-sam-with-receiver/2.3.0/gradle813": { + "jar": "sha256-r8iTlKUrUu8Lp6gpakJi0NOoo850CRXYfXE7CgFhMpg=" + }, + "org/jetbrains/kotlin#kotlin-script-runtime/2.2.20": { + "jar": "sha256-XIvV3Xrh6i7rJ3j9vqoZpWIYSXX2yrigu2d55BkHMa4=", + "pom": "sha256-IpOhQenagKfpjYXtKkkuldsMAWW86rC3Klzp4tkrCAc=" + }, + "org/jetbrains/kotlin#kotlin-script-runtime/2.3.0": { + "jar": "sha256-24JpYTcdZgUxjZxOS/zb+slMOgiSzcq9VSJIcP6tV/E=", + "pom": "sha256-20CN5tumaQeVvFOkoPhbM8en1qzLZ94ZAmQPr1QfL1s=" + }, + "org/jetbrains/kotlin#kotlin-scripting-common/2.2.20": { + "jar": "sha256-+5n/fwzZUtpo1UjT89ZErlB4sLlvybrwZewUZqKTuAU=", + "pom": "sha256-iTjGIFKXW7uW3OotqaeNS2sk2vLwnTWMGnqEHxaMtzo=" + }, + "org/jetbrains/kotlin#kotlin-scripting-common/2.3.0": { + "jar": "sha256-QlK3gs2lP8v9lTuQrwMlDiGX/+9uVavZsKLkj5Pv8lM=", + "pom": "sha256-9AuKrV3x68riUJLMM7hpP6Qw5TRCC6Wup/AGsvMrQw4=" + }, + "org/jetbrains/kotlin#kotlin-scripting-compiler-embeddable/2.2.20": { + "jar": "sha256-2GJhDAAzQuUIjKIcRAQix9ijcA4ZnWA/ehAnjESIR2E=", + "pom": "sha256-PW9vFZH6P3r14jFlxowu4BclFYZFQ09eMBdF5kfHVhE=" + }, + "org/jetbrains/kotlin#kotlin-scripting-compiler-embeddable/2.3.0": { + "jar": "sha256-ZxRdvqkxlNuyJT4vNaCp/ngBfCy84+zbSx/0P/9HGNU=", + "pom": "sha256-FjR61xI8BuiaUu+twxjZaick9UqBx+xZTA1ALh8eZFk=" + }, + "org/jetbrains/kotlin#kotlin-scripting-compiler-impl-embeddable/2.2.20": { + "jar": "sha256-e3kSNZL//r81xhag/xBDMscc3mN6ZX4JrbXfbD+cA84=", + "pom": "sha256-+l+wJ+4qSbPb/zh3VBtC+3CuzMN7oC4dOthipcZyVLc=" + }, + "org/jetbrains/kotlin#kotlin-scripting-compiler-impl-embeddable/2.3.0": { + "jar": "sha256-MsdCfxBeYtT07/MDXaHkKAsndxvkrWFDuoZV0Yh3IM0=", + "pom": "sha256-TPG5v5rmnHbz6nWnhW3GbeRGeKfHQXFa6cFdtA7Nfv0=" + }, + "org/jetbrains/kotlin#kotlin-scripting-jvm/2.2.20": { + "jar": "sha256-qR+5BJY0oyum09LpGgy5mD4KpOccDC4bKDg4qOBhYx8=", + "pom": "sha256-0df8RWSBne6v6OvdcfbyGBf/xVjr0U9HpW6NyTaW3Rk=" + }, + "org/jetbrains/kotlin#kotlin-scripting-jvm/2.3.0": { + "jar": "sha256-NpM+uDYZqKZeBB36m2ktNOB9V8b9Yv43HIu/BYgL7Ec=", + "pom": "sha256-qemsNTtIBUA7A3spmZpUnxvESATniVnYT/sbwzBF8kc=" + }, + "org/jetbrains/kotlin#kotlin-serialization-compiler-plugin-embeddable/2.2.20": { + "jar": "sha256-jY+TYgGErIdsuo09/aF8JdQD+Q0kacqHrNfvox2EeZ8=", + "pom": "sha256-fJa3mogscA0BKBr1iT0dkuknZX2AOHkGjYMQsNV5G8E=" + }, + "org/jetbrains/kotlin#kotlin-stdlib-jdk7/2.2.20": { + "jar": "sha256-O9Juy20Sl4xcTgtB92yf9VH6wqXmJoQnqdKgzfilrZE=", + "pom": "sha256-Cukeav/wZg79os8CjFvbnnoehn4Z3CyOuuiaglcUOOI=" + }, + "org/jetbrains/kotlin#kotlin-stdlib-jdk8/2.2.20": { + "jar": "sha256-wxQXeTXY3C7ah5UHEX8l1t5W9sV+3plBaxTNYiu54J0=", + "pom": "sha256-NditbBnaV6t00h7YHdxYSZt8GwAlEbXoUgANuwxYq64=" + }, + "org/jetbrains/kotlin#kotlin-stdlib/2.2.20": { + "jar": "sha256-iDbM/9NYX63amQEkSyDUKQHS881YEFjYQ04v+rzzo+c=", + "module": "sha256-yRj1IU0CGnLjdn8nVul9EDpSbgTxQj2jZj79+1hH25U=", + "pom": "sha256-SosIbmQxvPYjY39Ssv8ZLhrbkTg4dC5cDupwqN7kKcQ=" + }, + "org/jetbrains/kotlin#kotlin-stdlib/2.2.20/all": { + "jar": "sha256-VVzbTIhKWEwbXVn2lvt2zWYJF/qza4roAyvNYsdQ7+U=" + }, + "org/jetbrains/kotlin#kotlin-stdlib/2.3.0": { + "jar": "sha256-iHWHyRcTJQrVL+FK2RZtBCwzg1BJiQ6UN/NV/8WhlbE=", + "module": "sha256-CRCoo7aWD8eSxFxWqR18Oj8mKG8DKVVUtRnP83h1baI=", + "pom": "sha256-TVJW0+SETmVrDKQF9jUNbyF5XCQ3WzRSUmxUZ92ZtaI=" + }, + "org/jetbrains/kotlin#kotlin-tooling-core/2.3.0": { + "jar": "sha256-NnFCeBKZvA+RIMHe7A5ik0oa+ep/AaqpxaU1TcXY19k=", + "pom": "sha256-tQ6FtLEYwSIjge0c67K6lqfeLdrtti3aZ9SuBqGiXTc=" + }, + "org/jetbrains/kotlin#kotlin-util-io/2.3.0": { + "jar": "sha256-HJEgPyfnO5aI3f4aiAIyjTXrcPpV9eE96ttyHnj5KqQ=", + "pom": "sha256-hvmuNH2YfMwY2aEJ7tLlVDvjj/SMgdUtKMf6HyBarK8=" + }, + "org/jetbrains/kotlin#kotlin-util-klib-metadata/2.3.0": { + "jar": "sha256-JZjCbDTMXGnrAEc0Y+lQrmfpuAln9DoBg8Vn7eZqlxc=", + "pom": "sha256-4EB9YmkdWjQWFh/YNztNt0o714bxtILghGGXUQL58+s=" + }, + "org/jetbrains/kotlin#kotlin-util-klib/2.3.0": { + "jar": "sha256-ZLugCZZqAoGG2e5waw3ID+phwK5usXJe0dfXDvIrUuE=", + "pom": "sha256-lzWjPLZJg7qg0S3AyOGTSw5VLmvMh2chrFWKmCKFC/4=" + }, + "org/jetbrains/kotlin#swift-export-embeddable/2.2.20": { + "jar": "sha256-xAgKy0UComoaKdjxcbLjBIJKoiDCrtSvQEOHfy8tREg=", + "pom": "sha256-CiNVWKEHhN+VqqVTXkFEpscfSfpqCqGOL8sBKbaLG3Y=" + }, + "org/jetbrains/kotlinx#kotlinx-coroutines-bom/1.8.0": { + "pom": "sha256-Ejnp2+E5fNWXE0KVayURvDrOe2QYQuQ3KgiNz6i5rVU=" + }, + "org/jetbrains/kotlinx#kotlinx-coroutines-core-jvm/1.8.0": { + "jar": "sha256-mGCQahk3SQv187BtLw4Q70UeZblbJp8i2vaKPR9QZcU=", + "module": "sha256-/2oi2kAECTh1HbCuIRd+dlF9vxJqdnlvVCZye/dsEig=", + "pom": "sha256-pWM6vVNGfOuRYi2B8umCCAh3FF4LduG3V4hxVDSIXQs=" + }, + "org/jetbrains/kotlinx#kotlinx-serialization-bom/1.7.3": { + "pom": "sha256-QiakkcW1nOkJ9ztlqpiUQZHI3Kw4JWN8a+EGnmtYmkY=" + }, + "org/jetbrains/kotlinx#kotlinx-serialization-core-jvm/1.7.3": { + "jar": "sha256-8K3eRYZBREdThc9Kp+C3/rJ/Yfz5RyZl7ZjMlxsGses=", + "module": "sha256-c7tMAnk/h8Ke9kvqS6AlgHb01Mlj/NpjPRJI7yS0tO8=", + "pom": "sha256-c09fdJII3QvvPZjKpZTPkiKv3w/uW2hDNHqP5k4kBCc=" + }, + "org/jetbrains/kotlinx#kotlinx-serialization-core/1.7.3": { + "jar": "sha256-SFBoLg5ZdoYmlTMNhOuGmfHcXVCEn2JSY5lcyIvG83s=", + "module": "sha256-OdCabgLfKzJVhECmTGKPnGBfroxPYJAyF5gzTIIXfmQ=", + "pom": "sha256-MdERd2ua93fKFnED8tYfvuqjLa5t1mNZBrdtgni6VzA=" + }, + "org/jetbrains/kotlinx#kotlinx-serialization-json-jvm/1.7.3": { + "jar": "sha256-sekThJntjSA3Xt2j8rHJXzEDoljv9q+e3F6gcQDyspw=", + "module": "sha256-D/cOITHypldYIvdhHAXig8SuCBczA/QQSUy0Eom9PvY=", + "pom": "sha256-0zRdKAgXvgfpwnrNYHPUleF73/VxxHADTglmQgeGp90=" + }, + "org/jetbrains/kotlinx#kotlinx-serialization-json/1.7.3": { + "jar": "sha256-qpP6PJY5LLE5WTE0Qw3C1RNn9Z1VPl43R+vYAHsmPxs=", + "module": "sha256-HPAiijWIcx1rrzvLvbCKMiUB9wQg1Q4pKrUB5V2Mz08=", + "pom": "sha256-BaiftqSvoKHUB51YgsrTSaF/4IqYv5a30A0GplUh3H0=" + }, + "org/jspecify#jspecify/1.0.0": { + "jar": "sha256-H61ua+dVd4Hk0zcp1Jrhzcj92m/kd7sMxozjUer9+6s=", + "module": "sha256-0wfKd6VOGKwe8artTlu+AUvS9J8p4dL4E+R8J4KDGVs=", + "pom": "sha256-zauSmjuVIR9D0gkMXi0N/oRllg43i8MrNYQdqzJEM6Y=" + }, + "org/junit#junit-bom/5.11.2": { + "module": "sha256-iDoFuJLxGFnzg23nm3IH4kfhQSVYPMuKO+9Ni8D1jyw=", + "pom": "sha256-9I6IU4qsFF6zrgNFqevQVbKPMpo13OjR6SgTJcqbDqI=" + }, + "org/junit#junit-bom/5.11.3": { + "module": "sha256-S/D1PO6nx5D9+9JNujyeBM3FGGQnnuv8V6qkc+vKA4A=", + "pom": "sha256-8T3y5Mrx/rzlZ2Z+fDeBAaAzHVPRMk1uLf467Psfd3Q=" + }, + "org/junit/jupiter#junit-jupiter-api/5.11.3": { + "jar": "sha256-XYFHpg9JRTlz4lDtaHAbf/BVlk/iRi/Cyx7B1tRIibo=", + "module": "sha256-zC4yvwDuZDSpoZ3P2fJ6z2ZaPdYy03fkdhgNies+8n0=", + "pom": "sha256-8Zr+CSOwGTXEDy5+ltZgN+IAi3AXkXzHBRM4N3hJYY4=" + }, + "org/junit/jupiter#junit-jupiter-engine/5.11.3": { + "jar": "sha256-5iQgyZ98DVmiFZou9j5hh36cgL1yLAPKi/O9zqBQpYk=", + "module": "sha256-s3w9vEFSbrpeVMz/ROxUf0hVYstDyj0J2p+n2hmjBl4=", + "pom": "sha256-eJ43jTfEndhlTWbjrTreQY5YhK5vSHI5Ad0758nsngM=" + }, + "org/junit/jupiter#junit-jupiter-params/5.11.3": { + "jar": "sha256-D3mOvsdExOZgX9TyBy9BqOmJ4tRp4h21qmfPeZwLUew=", + "module": "sha256-sLUYC9HX9NFhsKCF+7JP2hbNcKfQX2Ni4asuSS0cq+w=", + "pom": "sha256-zOw0JKBxSUTGd7lP1QP9DByiQ84VxAFg1gzmKKr6Nf8=" + }, + "org/junit/jupiter#junit-jupiter/5.11.3": { + "jar": "sha256-rHV47+0WI2fD3cAGM44H1FcVEP2YZmQuqT1bnk7S9mU=", + "module": "sha256-a5pr3dlKOPEmUmh67HyBJisZkf6+vEjKmP6rxWOhKwE=", + "pom": "sha256-y+nzhaChO2/tjGxu0fFtxgWpJlfzslsZaDjHPZdoSAY=" + }, + "org/junit/platform#junit-platform-commons/1.11.3": { + "jar": "sha256-viYpZLC2tI3pd8YdT5Md+M9h6A51DMPzoKOc3SHBAIw=", + "module": "sha256-l531zqTESC/fxZCK3ItGq2q/ADbpmk0CzBjAdDyLggc=", + "pom": "sha256-gW69MkSncNkV2cHUDTtGUf40j0L4m3y369De4gnFIEA=" + }, + "org/junit/platform#junit-platform-engine/1.11.3": { + "jar": "sha256-AEP3L2EWZHNdqNyaMIvxLs0iNrBTOTUcR0HttNj6sNo=", + "module": "sha256-K5UnTIxw3eS9vEmQnxxY61qSLlQcXdO+qpx68rv6Qaw=", + "pom": "sha256-/ibcXakRuUtowSsiQSV6IIE1u7m4yRzBoTQzqAp6eR4=" + }, + "org/junit/platform#junit-platform-launcher/1.11.3": { + "jar": "sha256-tHJ0WSAbABG+sHQr2AdCGh/IQmsRYZMDHth4JbwtTwQ=", + "module": "sha256-cqqtIKPLIsFMA9WYDgJZZ1KmWe3EaylHH35c/yJWbow=", + "pom": "sha256-ElFDZ7k84oUOXa4jt8PWSjZuVMuLgjf5FNiD/Z26G/o=" + }, + "org/opentest4j#opentest4j/1.3.0": { + "jar": "sha256-SOLfY2yrZWPO1k3N/4q7I1VifLI27wvzdZhoLd90Lxs=", + "module": "sha256-SL8dbItdyU90ZSvReQD2VN63FDUCSM9ej8onuQkMjg0=", + "pom": "sha256-m/fP/EEPPoNywlIleN+cpW2dQ72TfjCUhwbCMqlDs1U=" + }, + "org/pkl-lang#pkl-cli-linux-aarch64/0.31.0": { + "pom": "sha256-x3gmFPR3mEb7PHdM+h//H76KFPT3wSrfKAePCZuL2bk=" + }, + "org/pkl-lang#pkl-cli-linux-amd64/0.31.0": { + "pom": "sha256-fW8qzg6gu30oAuGz9yl6TIIOsvsE9qfpuvKbtIHf1Vo=" + }, + "org/pkl-lang#pkl-cli-macos-aarch64/0.31.0": { + "pom": "sha256-TPv97UpFwv2mOd8VDaTcvpHE/wlvyNV0ixfObf0cImI=" + }, + "org/pkl-lang#pkl-cli-macos-amd64/0.31.0": { + "pom": "sha256-JapHevmskTA8G7W8oF4Khe/CztXNeR6i7TR6jmuMhyg=" + }, + "org/pkl-lang#pkl-formatter/0.31.0": { + "jar": "sha256-DiIZhJM4k9Ye94nQBWgpFStjgnLOFetnjHQ60FJ34Hc=", + "module": "sha256-AZQyJzxiZsVIPxtqtwnbUCG06RdN9/ZwBhbz8519MWU=", + "pom": "sha256-XAUFbC4GfHyosMiei6sHKrMGIFm4sL5NFQA53t822pI=" + }, + "org/pkl-lang#pkl-parser/0.31.0": { + "jar": "sha256-sjYkK4BB7nUO3+uB1wXcu9VVO4C8z5Pj1ANp3caSsB4=", + "module": "sha256-m7PyN1nv9/vN5QJ4Iu2q//P3WSNSMdvfzGinxnP/9Hc=", + "pom": "sha256-9Hsolbg6lyNPf4wSfq0TXoxLcHz+OAV1NWgUAFfvwfQ=" + }, + "org/pkl-lang#pkl-stdlib/0.31.0": { + "pom": "sha256-gTvfHz0BYSqaCst9rdvcZgRzrq/S87eKJGNgE8XGvNw=" + }, + "org/pkl-lang/pkl-cli-linux-aarch64/0.31.0/pkl-cli-linux-aarch64-0.31.0": { + "bin": "sha256-RxRgzdEeHLmsClQB/bBSd6462zpFc8wKnGPuCHwfk8g=" + }, + "org/pkl-lang/pkl-cli-linux-amd64/0.31.0/pkl-cli-linux-amd64-0.31.0": { + "bin": "sha256-WlwqiJtoypL/Qlj50nf5JBK5jf71BX2u91ZCAqIIcLY=" + }, + "org/pkl-lang/pkl-cli-macos-aarch64/0.31.0/pkl-cli-macos-aarch64-0.31.0": { + "bin": "sha256-NJQCrjLDU4LANLDAr3RP+w1TohOIjETe7JSngQ4USIk=" + }, + "org/pkl-lang/pkl-cli-macos-amd64/0.31.0/pkl-cli-macos-amd64-0.31.0": { + "bin": "sha256-nxzI46wjJ7xIO5DQwiDaIOt4XDuj/pLgIfR9PVZ2goI=" + }, + "org/pkl-lang/pkl-stdlib/0.31.0/pkl-stdlib-0.31.0": { + "zip": "sha256-hcKVlc79njRhm+RvgIKu2cgQ5X90UV1sRZm55Oxo36I=" + } + } +} diff --git a/pkgs/by-name/pk/pkl-lsp/package.nix b/pkgs/by-name/pk/pkl-lsp/package.nix new file mode 100644 index 000000000000..b3645dbbe16a --- /dev/null +++ b/pkgs/by-name/pk/pkl-lsp/package.nix @@ -0,0 +1,146 @@ +{ + lib, + stdenv, + fetchFromGitHub, + gradle_9, + jdk25_headless, + makeBinaryWrapper, + nix-update-script, + versionCheckHook, + zig, +}: + +let + jdk = jdk25_headless; + gradle = gradle_9; + gradleOverlay = gradle.override { java = jdk; }; +in +stdenv.mkDerivation (finalAttrs: { + pname = "pkl-lsp"; + version = "0.6.0"; + + src = fetchFromGitHub { + owner = "apple"; + repo = "pkl-lsp"; + tag = finalAttrs.version; + hash = "sha256-V6MrDpdh4jnSiXWD0UbF/XXpLa95smCbdj9/jT0Xb3w="; + leaveDotGit = true; + postFetch = '' + pushd $out + git rev-parse HEAD | tr -d '\n' > .commit-hash + rm -rf .git + popd + ''; + }; + + # Dependencies for tree-sitter compilation specific versions from gradle/libs.versions.toml + treeSitterSrc = fetchFromGitHub { + owner = "tree-sitter"; + repo = "tree-sitter"; + rev = "v0.25.3"; + hash = "sha256-xafeni6Z6QgPiKzvhCT2SyfPn0agLHo47y+6ExQXkzE="; + }; + treeSitterPklSrc = fetchFromGitHub { + owner = "apple"; + repo = "tree-sitter-pkl"; + rev = "v0.20.0"; + hash = "sha256-HfZ2NwO466Le2XFP1LZ2fLJgCq4Zq6hVpjChzsIoQgA="; + }; + + postPatch = '' + substituteInPlace buildSrc/src/main/kotlin/BuildInfo.kt \ + --replace-fail 'val jdkVersion: Int = 22' \ + 'val jdkVersion: Int = ${lib.versions.major jdk.version}' \ + --replace-fail 'val executable: Path get() = installDir.resolve(if (os.isWindows) "zig.exe" else "zig")' \ + 'val executable: Path get() = java.nio.file.Path.of("${lib.getExe zig}")' \ + + substituteInPlace build.gradle.kts \ + --replace-fail 'dependsOn(setupTreeSitterRepo)' "" \ + --replace-fail 'dependsOn(setupTreeSitterPklRepo)' "" \ + --replace-fail 'dependsOn(tasks.named("installZig"))' "" + + # Ensure all pkl-cli platform variants are cached + # Otherwise, deps.json only includes the current system's pkl-cli, and the tests fail + cat >> build.gradle.kts << 'GRADLE_PATCH' + val pklCliAllPlatforms by configurations.creating + dependencies { + for (platform in listOf("linux-amd64", "linux-aarch64", "macos-amd64", "macos-aarch64")) { + pklCliAllPlatforms("org.pkl-lang:pkl-cli-$platform:''${libs.versions.pkl.get()}") + } + } + GRADLE_PATCH + + mkdir -p build/repos/{tree-sitter,tree-sitter-pkl} + cp -r $treeSitterSrc/* build/repos/tree-sitter/ + cp -r $treeSitterPklSrc/* build/repos/tree-sitter-pkl/ + chmod +w -R build/repos/{tree-sitter,tree-sitter-pkl} + ''; + + nativeBuildInputs = [ + gradleOverlay + makeBinaryWrapper + zig + ]; + + mitmCache = gradle.fetchDeps { + inherit (finalAttrs) pname; + data = ./deps.json; + }; + + __darwinAllowLocalNetworking = true; + + gradleFlags = [ + "-DreleaseBuild=true" + "-Dfile.encoding=utf-8" + "-Porg.gradle.java.installations.auto-download=false" + "-Porg.gradle.java.installations.auto-detect=false" + ]; + + preBuild = '' + gradleFlagsArray+=(-DcommitId=$(cat .commit-hash)) + ''; + + # running the checkPhase replaces the .jar produced by the buildPhase, and leads to this error: + # Exception in thread "main" java.lang.NoClassDefFoundError: kotlin/jvm/internal/Intrinsics + # at org.pkl.lsp.cli.Main.main(Main.kt) + # Caused by: java.lang.ClassNotFoundException: kotlin.jvm.internal.Intrinsics + doCheck = false; + + postInstallCheck = '' + gradle test + ''; + + installPhase = '' + runHook preInstall + + install -D build/libs/pkl-lsp-${finalAttrs.version}.jar $out/lib/pkl-lsp/pkl-lsp.jar + + makeWrapper ${lib.getExe' jdk "java"} $out/bin/pkl-lsp \ + --add-flags "-jar $out/lib/pkl-lsp/pkl-lsp.jar" + + runHook postInstall + ''; + + nativeInstallCheckInputs = [ versionCheckHook ]; + doInstallCheck = true; + + passthru.updateScript = nix-update-script { }; + + meta = { + description = "The Pkl Language Server"; + homepage = "https://pkl-lang.org/lsp/current/index.html"; + downloadPage = "https://github.com/apple/pkl-lsp"; + changelog = "https://pkl-lang.org/lsp/current/CHANGELOG.html#release-${finalAttrs.version}"; + license = lib.licenses.asl20; + maintainers = with lib.maintainers; [ ryota2357 ]; + mainProgram = "pkl-lsp"; + platforms = lib.lists.intersectLists ( + lib.platforms.x86_64 ++ lib.platforms.aarch64 + ) jdk.meta.platforms; + sourceProvenance = with lib.sourceTypes; [ + fromSource + binaryBytecode # mitm cache + binaryNativeCode # mitm cache + ]; + }; +}) diff --git a/pkgs/by-name/re/remnote/package.nix b/pkgs/by-name/re/remnote/package.nix index 2dad794d5ad4..dfc430b58376 100644 --- a/pkgs/by-name/re/remnote/package.nix +++ b/pkgs/by-name/re/remnote/package.nix @@ -6,10 +6,10 @@ }: let pname = "remnote"; - version = "1.24.0"; + version = "1.24.7"; src = fetchurl { url = "https://download2.remnote.io/remnote-desktop2/RemNote-${version}.AppImage"; - hash = "sha256-OV8o2AOoDXdz02tXbtelcIOVOT3PIiBYJf38mRuvWdM="; + hash = "sha256-W4KM7QgkO+5Rr12IxlTlqp63LAakUJlMOX68JWBet6c="; }; appimageContents = appimageTools.extractType2 { inherit pname version src; }; in diff --git a/pkgs/by-name/ru/ruff/package.nix b/pkgs/by-name/ru/ruff/package.nix index 0ff8cbb186a7..72235a369e99 100644 --- a/pkgs/by-name/ru/ruff/package.nix +++ b/pkgs/by-name/ru/ruff/package.nix @@ -16,18 +16,18 @@ rustPlatform.buildRustPackage (finalAttrs: { pname = "ruff"; - version = "0.15.5"; + version = "0.15.6"; src = fetchFromGitHub { owner = "astral-sh"; repo = "ruff"; tag = finalAttrs.version; - hash = "sha256-bemgVXV/Bkp0aXmWX+R6Aas2/naOx0XEGp0ofh+vyyM="; + hash = "sha256-a8A9FdfrGCyg6TdunsZcXqAzeXb9pCWO/02f9Nl5juU="; }; cargoBuildFlags = [ "--package=ruff" ]; - cargoHash = "sha256-NaWWX6EAVkEg/KQ+Up0t2fh/24fnTo6i5dDZoOWErjg="; + cargoHash = "sha256-TD5FLdi4YJwDzJpCctNKYxUNj/VgMnB/OBp3exk3cZw="; nativeBuildInputs = [ installShellFiles ]; diff --git a/pkgs/by-name/s3/s3backer/package.nix b/pkgs/by-name/s3/s3backer/package.nix index 1fdae1bdc7f4..f34bcb9c9f43 100644 --- a/pkgs/by-name/s3/s3backer/package.nix +++ b/pkgs/by-name/s3/s3backer/package.nix @@ -36,6 +36,8 @@ stdenv.mkDerivation (finalAttrs: { 'AC_CHECK_DECLS(fdatasync)' "" ''; + env.CFLAGS = lib.optionalString stdenv.hostPlatform.isDarwin "-DFUSE_DARWIN_ENABLE_EXTENSIONS=0"; + meta = { homepage = "https://github.com/archiecobbs/s3backer"; description = "FUSE-based single file backing store via Amazon S3"; diff --git a/pkgs/by-name/sh/shogihome/package.nix b/pkgs/by-name/sh/shogihome/package.nix index 0f22f53f3eb8..1e5cb1add837 100644 --- a/pkgs/by-name/sh/shogihome/package.nix +++ b/pkgs/by-name/sh/shogihome/package.nix @@ -4,7 +4,7 @@ buildNpmPackage, fetchFromGitHub, makeWrapper, - electron_39, + electron_40, vulkan-loader, makeDesktopItem, copyDesktopItems, @@ -18,20 +18,20 @@ }: let - electron = electron_39; + electron = electron_40; in buildNpmPackage (finalAttrs: { pname = "shogihome"; - version = "1.26.1"; + version = "1.27.0"; src = fetchFromGitHub { owner = "sunfish-shogi"; repo = "shogihome"; tag = "v${finalAttrs.version}"; - hash = "sha256-7kDk85tN4uP0WJnof8yyn0M85Qairls5ZqhKwwhRQxc="; + hash = "sha256-T1MgcqCi9rwN86vgCAshokznMXh+masFLcO43sz2bo0="; }; - npmDepsHash = "sha256-Sft5fEf86o1uUJ+yszx9XgQBGNRc+9aKRyR5rOelgQw="; + npmDepsHash = "sha256-5tZQCxql6jZAEU+e/hkQYnaHy1l5dWaH/p2rbGDAX14="; postPatch = '' substituteInPlace package.json \ diff --git a/pkgs/by-name/ss/sshfs-fuse/common.nix b/pkgs/by-name/ss/sshfs-fuse/common.nix index 1689c9276fe6..ca4b1b111897 100644 --- a/pkgs/by-name/ss/sshfs-fuse/common.nix +++ b/pkgs/by-name/ss/sshfs-fuse/common.nix @@ -22,9 +22,6 @@ openssh, }: -let - fuse = if stdenv.hostPlatform.isDarwin then macfuse-stubs.override { isFuse3 = true; } else fuse3; -in stdenv.mkDerivation (finalAttrs: { pname = "sshfs-fuse"; inherit version; @@ -46,7 +43,7 @@ stdenv.mkDerivation (finalAttrs: { makeWrapper ]; buildInputs = [ - fuse + fuse3 glib ]; nativeCheckInputs = [ @@ -75,7 +72,7 @@ stdenv.mkDerivation (finalAttrs: { checkPhase = lib.optionalString (!stdenv.hostPlatform.isDarwin) '' # The tests need fusermount: mkdir bin - cp ${fuse}/bin/fusermount3 bin/fusermount + cp ${fuse3}/bin/fusermount3 bin/fusermount export PATH=bin:$PATH # Can't access /dev/fuse within the sandbox: "FUSE kernel module does not seem to be loaded" substituteInPlace test/util.py --replace "/dev/fuse" "/dev/null" diff --git a/pkgs/by-name/tr/triton-llvm/package.nix b/pkgs/by-name/tr/triton-llvm/package.nix index b8072db53644..a8e8f0d96259 100644 --- a/pkgs/by-name/tr/triton-llvm/package.nix +++ b/pkgs/by-name/tr/triton-llvm/package.nix @@ -199,6 +199,12 @@ stdenv.mkDerivation (finalAttrs: { # Not sure why this fails + lib.optionalString stdenv.hostPlatform.isAarch64 '' rm llvm/test/tools/llvm-exegesis/AArch64/latency-by-opcode-name.s + '' + # The second llvm-install-name-tool invocation fails with + # "is not a Mach-O file" on aarch64-linux, even on a fresh copy of + # the original yaml2obj output. Root cause unknown. + + lib.optionalString stdenv.hostPlatform.isAarch64 '' + rm llvm/test/tools/llvm-objcopy/MachO/install-name-tool-change.test ''; postInstall = '' diff --git a/pkgs/by-name/va/vacuum-tube/package.nix b/pkgs/by-name/va/vacuum-tube/package.nix index cb078263c739..8e996458adf5 100644 --- a/pkgs/by-name/va/vacuum-tube/package.nix +++ b/pkgs/by-name/va/vacuum-tube/package.nix @@ -10,16 +10,16 @@ buildNpmPackage rec { pname = "vacuum-tube"; - version = "1.6.0"; + version = "1.6.1"; src = fetchFromGitHub { owner = "shy1132"; repo = "VacuumTube"; tag = "v${version}"; - hash = "sha256-BnFI517pXKsHQ8AJMRzAlXBTLMLhjyEasIhZdSHtyC0="; + hash = "sha256-DbcJJ9FL9LPCQrg6lGa5N9KC8stXLdaMI+2hKAiZFxQ="; }; - npmDepsHash = "sha256-R7DISsTJv/DDi8uJTWF+6/P8K86BguxtZNsaL2qCxhY="; + npmDepsHash = "sha256-/TAGGiNuT7YC29U9n6M+zD51kecbAPXzzEJU5ey1hXs="; makeCacheWritable = true; env = { diff --git a/pkgs/by-name/vo/volanta/package.nix b/pkgs/by-name/vo/volanta/package.nix index 50e195594a5e..49710588a6b8 100644 --- a/pkgs/by-name/vo/volanta/package.nix +++ b/pkgs/by-name/vo/volanta/package.nix @@ -9,11 +9,11 @@ }: let pname = "volanta"; - version = "1.15.3"; - build = "64ba2e0c"; + version = "1.16.3"; + build = "581a1e68"; src = fetchurl { url = "https://cdn.volanta.app/software/volanta-app/${version}-${build}/volanta-${version}.AppImage"; - hash = "sha256-rTonFExYHXLuRWf98IsNE7KqGrRMRC+Hke6CGKJWLAA="; + hash = "sha256-5187tE37dRyqjBa8P0Jwio2lBd8qd+tEZgl/98nGQy8="; }; appImageContents = appimageTools.extract { inherit pname version src; }; in diff --git a/pkgs/by-name/vu/vultisig-cli/package.nix b/pkgs/by-name/vu/vultisig-cli/package.nix index 725b2390e027..4dcaaf14a145 100644 --- a/pkgs/by-name/vu/vultisig-cli/package.nix +++ b/pkgs/by-name/vu/vultisig-cli/package.nix @@ -9,20 +9,20 @@ stdenv.mkDerivation (finalAttrs: { pname = "vultisig-cli"; - version = "0.5.0"; + version = "0.6.0"; src = fetchFromGitHub { owner = "vultisig"; repo = "vultisig-sdk"; tag = "v${finalAttrs.version}"; - hash = "sha256-vpWoKxdiUSSI8xGYXmnduJnB3zB3jpBMxz+9eGXJgvM="; + hash = "sha256-eQvWD0Jubtp0wfmuTBN4Mr4rKqoEvMiAGI5D8GAHYDY="; }; missingHashes = ./missing-hashes.json; offlineCache = yarn-berry.fetchYarnBerryDeps { inherit (finalAttrs) src missingHashes; - hash = "sha256-ZJfLfaTvJKyCh4FtOs7IyZskBBjrJLjI0/9hphclFvU="; + hash = "sha256-SQ2C01dVSzJwzCvJUclcSiGTPz7RJfO3fYPCZbvnAHk="; }; nativeBuildInputs = [ diff --git a/pkgs/by-name/ze/zed-discord-presence/package.nix b/pkgs/by-name/ze/zed-discord-presence/package.nix index ec1c28e30c40..3d99195c7d62 100644 --- a/pkgs/by-name/ze/zed-discord-presence/package.nix +++ b/pkgs/by-name/ze/zed-discord-presence/package.nix @@ -7,17 +7,17 @@ rustPlatform.buildRustPackage (finalAttrs: { pname = "zed-discord-presence"; - version = "0.10.1"; + version = "0.11.0"; src = fetchFromGitHub { owner = "xhyrom"; repo = "zed-discord-presence"; tag = "v${finalAttrs.version}"; - hash = "sha256-7bTLMrvcgT5/ziyD4ieDGx7218rezQToHqAwEAwYB/E="; + hash = "sha256-HmSJipRWVB1rXyO5ZK1ksyCLDzSJD820Klo88A7NLx4="; }; cargoBuildFlags = [ "--package discord-presence-lsp" ]; - cargoHash = "sha256-cOG9VeKdi1mSYpOPGYXLwHksEKJypvSK6vMFaO7TYBg="; + cargoHash = "sha256-x9sB90jW7v2SGggLILgLbBfFV7DkJazcrUiKAfIroMA="; passthru.updateScript = nix-update-script { }; diff --git a/pkgs/development/coq-modules/CakeMLExtraction/default.nix b/pkgs/development/coq-modules/CakeMLExtraction/default.nix new file mode 100644 index 000000000000..320af5310043 --- /dev/null +++ b/pkgs/development/coq-modules/CakeMLExtraction/default.nix @@ -0,0 +1,58 @@ +{ + lib, + mkCoqDerivation, + coq, + ceres-bs, + equations, + metarocq-erasure-plugin, + version ? null, +}: + +(mkCoqDerivation { + pname = "CakeMLExtraction"; + owner = "peregrine-project"; + repo = "cakeml-backend"; + opam-name = "rocq-cakeml-extraction"; + + inherit version; + defaultVersion = + let + case = coq: mr: out: { + cases = [ + coq + mr + ]; + inherit out; + }; + in + with lib.versions; + lib.switch + [ + coq.coq-version + metarocq-erasure-plugin.version + ] + [ + (case (range "9.0" "9.1") (range "1.4" "1.5.1") "0.1.0") + ] + null; + release = { + "0.1.0".sha256 = "sha256-diDUTj0l4vliov9+Lg8lNRdkLE7JAfJn8OU7J/HgmDE="; + }; + releaseRev = v: "v${v}"; + + mlPlugin = false; + useDune = false; + + buildInputs = [ + equations + metarocq-erasure-plugin + ceres-bs + ]; + propagatedBuildInputs = [ coq.ocamlPackages.findlib ]; + + meta = with lib; { + homepage = "https://peregrine-project.github.io/"; + description = "CakeML backend for Peregrine"; + maintainers = with maintainers; [ _4ever2 ]; + }; +}) diff --git a/pkgs/development/coq-modules/CertiRocq/default.nix b/pkgs/development/coq-modules/CertiRocq/default.nix new file mode 100644 index 000000000000..cb7f5accee56 --- /dev/null +++ b/pkgs/development/coq-modules/CertiRocq/default.nix @@ -0,0 +1,101 @@ +{ + lib, + pkgs, + pkg-config, + mkCoqDerivation, + coq, + wasmcert, + compcert, + metarocq-erasure-plugin, + metarocq-safechecker-plugin, + ExtLib, + version ? null, +}: + +with lib; +mkCoqDerivation { + pname = "CertiRocq"; + owner = "CertiRocq"; + repo = "certirocq"; + opam-name = "rocq-certirocq"; + mlPlugin = true; + + inherit version; + defaultVersion = + let + case = coq: mr: out: { + cases = [ + coq + mr + ]; + inherit out; + }; + in + lib.switch + [ + coq.coq-version + metarocq-erasure-plugin.version + ] + [ + (case "9.1" "1.5.1-9.1" "0.9.1+9.1") + ] + null; + release = { + "0.9.1+9.1".sha256 = "sha256-YsweBaoq8+QG63e7Llp/4bHldAFnSQSyMumJkb+Bsp0="; + }; + releaseRev = v: "v${v}"; + + buildInputs = [ + pkgs.clang + ]; + + propagatedBuildInputs = [ + wasmcert + compcert + ExtLib + metarocq-erasure-plugin + metarocq-safechecker-plugin + ]; + + patchPhase = '' + patchShebangs ./configure.sh + patchShebangs ./clean_extraction.sh + patchShebangs ./make_plugin.sh + ''; + + configurePhase = '' + ./configure.sh local + ''; + + buildPhase = '' + runHook preBuild + + make all + make plugins + + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + + OUTDIR=$out/lib/coq/${coq.coq-version}/user-contrib + + DST=$OUTDIR/CertiRocq/Plugin/runtime make -C runtime install + COQLIBINSTALL=$OUTDIR make -C theories install + COQLIBINSTALL=$OUTDIR make -C libraries install + COQLIBINSTALL=$OUTDIR COQPLUGININSTALL=$OCAMLFIND_DESTDIR make -C plugin install + COQLIBINSTALL=$OUTDIR COQPLUGININSTALL=$OCAMLFIND_DESTDIR make -C cplugin install + + runHook postInstall + ''; + + meta = { + description = "CertiRocq"; + maintainers = with maintainers; [ + womeier + _4ever2 + ]; + license = licenses.mit; + }; +} diff --git a/pkgs/development/interpreters/love/11.nix b/pkgs/development/interpreters/love/11.nix index 15ef843872fc..c94536743167 100644 --- a/pkgs/development/interpreters/love/11.nix +++ b/pkgs/development/interpreters/love/11.nix @@ -8,7 +8,6 @@ libGL, openal, luajit, - lua5_1, freetype, physfs, libmodplug, @@ -22,14 +21,14 @@ cmake, }: -stdenv.mkDerivation rec { +stdenv.mkDerivation (finalAttrs: { pname = "love"; version = "11.5"; src = fetchFromGitHub { owner = "love2d"; repo = "love"; - rev = version; + tag = finalAttrs.version; sha256 = "sha256-wZktNh4UB3QH2wAIIlnYUlNoXbjEDwUmPnT4vesZNm0="; }; @@ -40,7 +39,7 @@ stdenv.mkDerivation rec { buildInputs = [ SDL2 openal - (if stdenv.isDarwin then lua5_1 else luajit) + luajit freetype physfs libmodplug @@ -51,7 +50,7 @@ stdenv.mkDerivation rec { which libtool ] - ++ lib.optionals stdenv.isLinux [ + ++ lib.optionals stdenv.hostPlatform.isLinux [ libx11 # SDL2 optional depend, for SDL_syswm.h libGLU libGL @@ -61,9 +60,10 @@ stdenv.mkDerivation rec { # On Darwin, autotools doesn't compile macOS-specific module (src/common/macosx.mm), # leading to stubbed functions and segfaults cmakeFlags = [ - "-DCMAKE_POLICY_VERSION_MINIMUM=3.5" # Required by LÖVE's CMakeLists.txt - "-DCMAKE_SKIP_BUILD_RPATH=ON" # Don't include build directory in RPATH - "-DCMAKE_BUILD_WITH_INSTALL_RPATH=ON" # Use install RPATH even during build + (lib.cmakeFeature "CMAKE_POLICY_VERSION_MINIMUM" "3.5") # Required by LÖVE's CMakeLists.txt + (lib.cmakeBool "CMAKE_SKIP_BUILD_RPATH" true) # Don't include build directory in RPATH + (lib.cmakeBool "CMAKE_BUILD_WITH_INSTALL_RPATH" true) # Use install RPATH even during build + (lib.cmakeBool "LOVE_JIT" true) # Enable LuaJIT support even though it is warned about for Apple ]; env.NIX_CFLAGS_COMPILE = "-DluaL_reg=luaL_Reg"; # needed since luajit-2.1.0-beta3 @@ -81,7 +81,7 @@ stdenv.mkDerivation rec { cp $src/platform/unix/love.6 $out/share/man/man1/love.1 gzip -9n $out/share/man/man1/love.1 '' - + lib.optionalString stdenv.isLinux '' + + lib.optionalString stdenv.hostPlatform.isLinux '' # Install Linux-specific files (desktop, mime, icons) mkdir -p $out/share/applications mkdir -p $out/share/mime/packages @@ -112,4 +112,4 @@ stdenv.mkDerivation rec { platforms = lib.platforms.linux ++ lib.platforms.darwin; maintainers = [ lib.maintainers.raskin ]; }; -} +}) diff --git a/pkgs/development/python-modules/gvm-tools/default.nix b/pkgs/development/python-modules/gvm-tools/default.nix index be4bab25400c..cc9b7d1f5e52 100644 --- a/pkgs/development/python-modules/gvm-tools/default.nix +++ b/pkgs/development/python-modules/gvm-tools/default.nix @@ -9,14 +9,14 @@ buildPythonPackage (finalAttrs: { pname = "gvm-tools"; - version = "25.4.8"; + version = "25.4.9"; pyproject = true; src = fetchFromGitHub { owner = "greenbone"; repo = "gvm-tools"; tag = "v${finalAttrs.version}"; - hash = "sha256-tKUaUo9Sr4d9mLdbbmn+OmAgUcEuwWSzCYY4BPJ4UKw="; + hash = "sha256-dt7njGUqi6zfwUz0gSdOHWnSUJ+yJ7qJ3RttoPweR3c="; }; __darwinAllowLocalNetworking = true; diff --git a/pkgs/development/python-modules/osc/default.nix b/pkgs/development/python-modules/osc/default.nix index 66107e0999f3..b957a8e3bd80 100644 --- a/pkgs/development/python-modules/osc/default.nix +++ b/pkgs/development/python-modules/osc/default.nix @@ -13,14 +13,14 @@ buildPythonPackage rec { pname = "osc"; - version = "1.24.0"; + version = "1.25.0"; format = "setuptools"; src = fetchFromGitHub { owner = "openSUSE"; repo = "osc"; rev = version; - hash = "sha256-EPt+HTvDhBEs1sf1yrG+aawRcP1yd/+kY4OTeVHHFt4="; + hash = "sha256-ES4HhWlJx7fRf9rXWBeAANyCy1eC1Rz6yFczXvQ66Vo="; }; buildInputs = [ bashInteractive ]; # needed for bash-completion helper diff --git a/pkgs/development/python-modules/polyswarm-api/default.nix b/pkgs/development/python-modules/polyswarm-api/default.nix index 28c86011fe8b..3d4c6c99a9f3 100644 --- a/pkgs/development/python-modules/polyswarm-api/default.nix +++ b/pkgs/development/python-modules/polyswarm-api/default.nix @@ -13,14 +13,14 @@ buildPythonPackage (finalAttrs: { pname = "polyswarm-api"; - version = "3.16.0"; + version = "3.17.1"; pyproject = true; src = fetchFromGitHub { owner = "polyswarm"; repo = "polyswarm-api"; tag = finalAttrs.version; - hash = "sha256-mdsgHwbGThy2Lzvgzb0mItwJkNspLiqGZzBGGuQdatM="; + hash = "sha256-nL+DolLBnw/yahNqeCYtepAMFw4yHWXTajz3+KxF9R8="; }; build-system = [ setuptools ]; diff --git a/pkgs/development/python-modules/pyfuse3/default.nix b/pkgs/development/python-modules/pyfuse3/default.nix index c7b2a766feea..96af3308d5b0 100644 --- a/pkgs/development/python-modules/pyfuse3/default.nix +++ b/pkgs/development/python-modules/pyfuse3/default.nix @@ -62,5 +62,6 @@ buildPythonPackage rec { dotlambda ]; changelog = "https://github.com/libfuse/pyfuse3/blob/${src.tag}/Changes.rst"; + platforms = lib.platforms.linux; }; } diff --git a/pkgs/servers/nosql/mongodb/7.0.nix b/pkgs/servers/nosql/mongodb/7.0.nix index ade835d7c432..169b829cc7b8 100644 --- a/pkgs/servers/nosql/mongodb/7.0.nix +++ b/pkgs/servers/nosql/mongodb/7.0.nix @@ -21,8 +21,8 @@ let in buildMongoDB { inherit avxSupport; - version = "7.0.30"; - hash = "sha256-z0stPphy9EliDkanlDCHJ+ck3p1gUTMQ83uOW7kvlmI="; + version = "7.0.31"; + hash = "sha256-Vk/XsnYut0Hfad/X6LZw6gJX1NHc4/6XT8y1KehpLMk="; patches = [ # ModuleNotFoundError: No module named 'mongo_tooling_metrics': # NameError: name 'SConsToolingMetrics' is not defined: diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix index a04d9a851e00..5dbc6f34a623 100644 --- a/pkgs/top-level/all-packages.nix +++ b/pkgs/top-level/all-packages.nix @@ -8555,7 +8555,12 @@ with pkgs; ); fuse = fuse2; fuse2 = lowPrio (if stdenv.hostPlatform.isDarwin then macfuse-stubs else fusePackages.fuse_2); - fuse3 = fusePackages.fuse_3; + fuse3 = lowPrio ( + if stdenv.hostPlatform.isDarwin then + macfuse-stubs.override { isFuse3 = true; } + else + fusePackages.fuse_3 + ); gpm-ncurses = gpm.override { withNcurses = true; }; diff --git a/pkgs/top-level/coq-packages.nix b/pkgs/top-level/coq-packages.nix index 22ed5cc6f4b1..b0204719a753 100644 --- a/pkgs/top-level/coq-packages.nix +++ b/pkgs/top-level/coq-packages.nix @@ -68,9 +68,11 @@ let callPackage ../development/coq-modules/bignums { } else null; + CakeMLExtraction = callPackage ../development/coq-modules/CakeMLExtraction { }; category-theory = callPackage ../development/coq-modules/category-theory { }; ceres = callPackage ../development/coq-modules/ceres { }; ceres-bs = callPackage ../development/coq-modules/ceres-bs { }; + CertiRocq = callPackage ../development/coq-modules/CertiRocq { }; Cheerios = callPackage ../development/coq-modules/Cheerios { }; coinduction = callPackage ../development/coq-modules/coinduction { }; CoLoR = callPackage ../development/coq-modules/CoLoR (