Merge staging-next into staging

This commit is contained in:
nixpkgs-ci[bot]
2026-03-19 12:13:19 +00:00
committed by GitHub
89 changed files with 4515 additions and 1552 deletions
@@ -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 `<yourmachine>.shell_interact()` grants access to a shell running
inside a virtual machine. To use it, replace `<yourmachine>` 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`
@@ -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" ];
}
```
@@ -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.<name>`](#test-opt-nodes) are NixOS modules themselves.
in [`nodes.<name>`](#test-opt-nodes) and [`containers.<name>`](#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.<name>` 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 <PID>'
```
You can then enter the sandbox shell:
```
$ sudo /nix/store/eeeee-attach/bin/attach <PID>
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 <id>
bash# telnet 127.0.0.1 4444
pdb$ …
$ sudo /nix/store/eeeee-attach <PID>
bash# ssh -o User=root -o ProxyCommand="socat - UNIX-CLIENT:/run/systemd/nspawn/unix-export/machine/ssh" bash
[root@machine:~]# hostname
machine
```
+52 -3
View File
@@ -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"
@@ -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`.
+7
View File
@@ -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
+49 -15
View File
@@ -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()
@@ -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()))
+119 -30
View File
@@ -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 <nixos/lib/testing/network.nix>.
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,
)
File diff suppressed because it is too large Load Diff
@@ -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()
+5 -4
View File
@@ -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
+1
View File
@@ -56,6 +56,7 @@ pkgs.lib.throwIf (args ? specialArgs)
{
machine ? null,
nodes ? { },
containers ? { },
testScript,
enableOCR ? false,
globalTimeout ? (60 * 60),
+35 -13
View File
@@ -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}' \
+82 -50
View File
@@ -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.<name>`; stable across `specialisations`.
The `name` in `nodes.<name>` and `containers.<name>`; 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
];
};
};
}
+3 -2
View File
@@ -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)
);
}
)
+157 -40
View File
@@ -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.<name>.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.<name>.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
+4
View File
@@ -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 = ''
+2 -1
View File
@@ -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
+1
View File
@@ -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
@@ -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";
+147
View File
@@ -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;
}
+16 -2
View File
@@ -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.
'';
}
];
@@ -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
@@ -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 =
@@ -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,
+3 -3
View File
@@ -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;
@@ -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)
'';
}
@@ -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")
'';
}
+215
View File
@@ -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)
'';
}
@@ -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()
+153
View File
@@ -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;
}
@@ -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";
@@ -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 {
@@ -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=",
@@ -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}
@@ -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 = "";
+4 -4
View File
@@ -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/";
+3 -3
View File
@@ -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 = [
@@ -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 <http://www.gnu.org/licenses/>.
#define FUSE_USE_VERSION 31
+#ifdef __APPLE__
+#define FUSE_DARWIN_ENABLE_EXTENSIONS 0
+#endif
+
#include <cstdlib>
#include <iostream>
#include <fstream>
@@ -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;
+5
View File
@@ -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
+2 -2
View File
@@ -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 = [
+3 -3
View File
@@ -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" ];
+3 -3
View File
@@ -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";
+2
View File
@@ -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";
+2 -2
View File
@@ -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
+3 -3
View File
@@ -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"
+2 -2
View File
@@ -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 ];
+6 -3
View File
@@ -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
@@ -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) {
}
}
+5 -10
View File
@@ -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";
+77
View File
@@ -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 ];
};
})
+60 -65
View File
@@ -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";
};
})
+3 -3
View File
@@ -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 ]
+2 -2
View File
@@ -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 = [
+4 -1
View File
@@ -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";
+44 -44
View File
@@ -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",
+3 -3
View File
@@ -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 = ''
@@ -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 = [
@@ -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:
@@ -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,
@@ -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",
],
@@ -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)
@@ -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(
@@ -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(
[
+2 -2
View File
@@ -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 {
+3 -3
View File
@@ -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=";
+1021
View File
File diff suppressed because it is too large Load Diff
+146
View File
@@ -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
];
};
})
+2 -2
View File
@@ -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
+3 -3
View File
@@ -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 ];
+2
View File
@@ -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";
+5 -5
View File
@@ -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 \
+2 -5
View File
@@ -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"
+6
View File
@@ -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 = ''
+3 -3
View File
@@ -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 = {
+3 -3
View File
@@ -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
+3 -3
View File
@@ -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 = [
@@ -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 { };
@@ -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 ];
};
})
@@ -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;
};
}
+10 -10
View File
@@ -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 ];
};
}
})
@@ -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;
@@ -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
@@ -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 ];
@@ -62,5 +62,6 @@ buildPythonPackage rec {
dotlambda
];
changelog = "https://github.com/libfuse/pyfuse3/blob/${src.tag}/Changes.rst";
platforms = lib.platforms.linux;
};
}
+2 -2
View File
@@ -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:
+6 -1
View File
@@ -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; };
+2
View File
@@ -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 (