1
0
forked from clan/clan-core

replace environment variable with nixpkgs directory

In this directory we generate all the files that we need to load nixpkgs.
This seems more robust than all those environment variables that may or not may be set.
This commit is contained in:
Jörg Thalheim 2023-08-26 11:23:15 +02:00
parent dec5e1e5db
commit fb7c77690a
10 changed files with 152 additions and 93 deletions

1
.gitignore vendored
View File

@ -1,5 +1,6 @@
.direnv .direnv
result* result*
pkgs/clan-cli/clan_cli/nixpkgs
# python # python
__pycache__ __pycache__

View File

@ -24,3 +24,19 @@ def user_config_dir() -> Path:
return Path(os.path.expanduser("~/Library/Application Support/")) return Path(os.path.expanduser("~/Library/Application Support/"))
else: else:
return Path(os.getenv("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))) return Path(os.getenv("XDG_CONFIG_HOME", os.path.expanduser("~/.config")))
def module_root() -> Path:
return Path(__file__).parent
def flake_registry() -> Path:
return module_root() / "nixpkgs" / "flake-registry.json"
def nixpkgs() -> Path:
return (module_root() / "nixpkgs" / "path").resolve()
def unfree_nixpkgs() -> Path:
return module_root() / "nixpkgs" / "unfree"

View File

@ -1,10 +1,42 @@
import os import os
from .dirs import flake_registry, unfree_nixpkgs
def nix_shell(packages: list[str], cmd: list[str]) -> list[str]: def nix_shell(packages: list[str], cmd: list[str]) -> list[str]:
flake = os.environ.get("CLAN_FLAKE") # we cannot use nix-shell inside the nix sandbox
# in unittest we will have all binaries provided # in our tests we just make sure we have all the packages
if flake is None: if os.environ.get("IN_NIX_SANDBOX"):
return cmd return cmd
wrapped_packages = [f"path:{flake}#{p}" for p in packages] wrapped_packages = [f"nixpkgs#{p}" for p in packages]
return ["nix", "shell"] + wrapped_packages + ["-c"] + cmd return (
[
"nix",
"shell",
"--extra-experimental-features",
"nix-command flakes",
"--flake-registry",
str(flake_registry()),
]
+ wrapped_packages
+ ["-c"]
+ cmd
)
def unfree_nix_shell(packages: list[str], cmd: list[str]) -> list[str]:
if os.environ.get("IN_NIX_SANDBOX"):
return cmd
return (
[
"nix",
"shell",
"--extra-experimental-features",
"nix-command flakes",
"-f",
str(unfree_nixpkgs()),
]
+ packages
+ ["-c"]
+ cmd
)

View File

@ -9,7 +9,7 @@ from tempfile import TemporaryDirectory
from typing import Any, Iterator, Optional from typing import Any, Iterator, Optional
from ..errors import ClanError from ..errors import ClanError
from ..nix import nix_shell from ..nix import nix_shell, unfree_nix_shell
def try_bind_port(port: int) -> bool: def try_bind_port(port: int) -> bool:
@ -87,7 +87,10 @@ def zerotier_controller() -> Iterator[ZerotierController]:
controller_port = find_free_port(range(10000, 65535)) controller_port = find_free_port(range(10000, 65535))
if controller_port is None: if controller_port is None:
raise ClanError("cannot find a free port for zerotier controller") raise ClanError("cannot find a free port for zerotier controller")
cmd = nix_shell(["bash", "zerotierone"], ["bash", "-c", "command -v zerotier-one"])
cmd = unfree_nix_shell(
["bash", "zerotierone"], ["bash", "-c", "command -v zerotier-one"]
)
res = subprocess.run( res = subprocess.run(
cmd, cmd,
check=True, check=True,
@ -102,7 +105,6 @@ def zerotier_controller() -> Iterator[ZerotierController]:
raise ClanError( raise ClanError(
f"zerotier-one executable needs to come from /nix/store: {zerotier_exe}" f"zerotier-one executable needs to come from /nix/store: {zerotier_exe}"
) )
with TemporaryDirectory() as d: with TemporaryDirectory() as d:
tempdir = Path(d) tempdir = Path(d)
home = tempdir / "zerotier-one" home = tempdir / "zerotier-one"

View File

@ -48,6 +48,17 @@ let
chmod -R +w $out chmod -R +w $out
rm $out/clan_cli/config/jsonschema rm $out/clan_cli/config/jsonschema
cp -r ${self + /lib/jsonschema} $out/clan_cli/config/jsonschema cp -r ${self + /lib/jsonschema} $out/clan_cli/config/jsonschema
ln -s ${nixpkgs} $out/clan_cli/nixpkgs
'';
nixpkgs = runCommand "nixpkgs" { } ''
mkdir -p $out/unfree
cat > $out/unfree/default.nix <<EOF
import "${pkgs.path}" { config = { allowUnfree = true; overlays = []; }; }
EOF
cat > $out/flake-registry.json <<EOF
{ "flakes": [{"exact":true,"from":{"id":"nixpkgs", "type": "indirect"},"to": {"path":"${pkgs.path}", "type":"path"}}], "version": 2}
EOF
ln -s ${pkgs.path} $out/path
''; '';
in in
python3.pkgs.buildPythonPackage { python3.pkgs.buildPythonPackage {
@ -56,8 +67,6 @@ python3.pkgs.buildPythonPackage {
format = "pyproject"; format = "pyproject";
inherit CLAN_OPTIONS_FILE; inherit CLAN_OPTIONS_FILE;
# This is required for the python tests, where some nix libs depend on nixpkgs
CLAN_NIXPKGS = pkgs.path;
nativeBuildInputs = [ nativeBuildInputs = [
setuptools setuptools
@ -68,13 +77,13 @@ python3.pkgs.buildPythonPackage {
passthru.tests.clan-pytest = runCommand "clan-tests" passthru.tests.clan-pytest = runCommand "clan-tests"
{ {
nativeBuildInputs = [ age zerotierone bubblewrap sops nix openssh rsync stdenv.cc ]; nativeBuildInputs = [ age zerotierone bubblewrap sops nix openssh rsync stdenv.cc ];
CLAN_NIXPKGS = pkgs.path;
} '' } ''
export CLAN_OPTIONS_FILE="${CLAN_OPTIONS_FILE}" export CLAN_OPTIONS_FILE="${CLAN_OPTIONS_FILE}"
cp -r ${source} ./src cp -r ${source} ./src
chmod +w -R ./src chmod +w -R ./src
cd ./src cd ./src
NIX_STATE_DIR=$TMPDIR/nix ${checkPython}/bin/python -m pytest -s ./tests
NIX_STATE_DIR=$TMPDIR/nix IN_NIX_SANDBOX=1 ${checkPython}/bin/python -m pytest -s ./tests
touch $out touch $out
''; '';
passthru.clan-openapi = runCommand "clan-openapi" { } '' passthru.clan-openapi = runCommand "clan-openapi" { } ''
@ -84,6 +93,7 @@ python3.pkgs.buildPythonPackage {
${checkPython}/bin/python ./bin/gen-openapi --out $out/openapi.json --app-dir . clan_cli.webui.app:app ${checkPython}/bin/python ./bin/gen-openapi --out $out/openapi.json --app-dir . clan_cli.webui.app:app
touch $out touch $out
''; '';
passthru.nixpkgs = nixpkgs;
passthru.devDependencies = [ passthru.devDependencies = [
setuptools setuptools
@ -92,11 +102,8 @@ python3.pkgs.buildPythonPackage {
passthru.testDependencies = dependencies ++ testDependencies; passthru.testDependencies = dependencies ++ testDependencies;
makeWrapperArgs = [
"--set CLAN_FLAKE ${self}"
];
postInstall = '' postInstall = ''
ln -s ${nixpkgs} $out/${python3.sitePackages}/nixpkgs
installShellCompletion --bash --name clan \ installShellCompletion --bash --name clan \
<(${argcomplete}/bin/register-python-argcomplete --shell bash clan) <(${argcomplete}/bin/register-python-argcomplete --shell bash clan)
installShellCompletion --fish --name clan.fish \ installShellCompletion --fish --name clan.fish \

View File

@ -8,8 +8,8 @@ description = "cLAN CLI tool"
dynamic = [ "version" ] dynamic = [ "version" ]
scripts = { clan = "clan_cli:main" } scripts = { clan = "clan_cli:main" }
[tool.setuptools.packages] [tool.setuptools.packages.find]
find = {} exclude = ["clan_cli.nixpkgs*"]
[tool.setuptools.package-data] [tool.setuptools.package-data]
clan_cli = ["config/jsonschema/*"] clan_cli = ["config/jsonschema/*"]
@ -18,13 +18,13 @@ clan_cli = ["config/jsonschema/*"]
addopts = "--cov . --cov-report term --cov-report html:.reports/html --no-cov-on-fail" addopts = "--cov . --cov-report term --cov-report html:.reports/html --no-cov-on-fail"
norecursedirs = "tests/helpers" norecursedirs = "tests/helpers"
[tool.mypy] [tool.mypy]
python_version = "3.10" python_version = "3.10"
warn_redundant_casts = true warn_redundant_casts = true
disallow_untyped_calls = true disallow_untyped_calls = true
disallow_untyped_defs = true disallow_untyped_defs = true
no_implicit_optional = true no_implicit_optional = true
exclude = "clan_cli.nixpkgs"
[[tool.mypy.overrides]] [[tool.mypy.overrides]]
module = "argcomplete.*" module = "argcomplete.*"

View File

@ -18,29 +18,20 @@ pkgs.mkShell {
self.packages.${pkgs.system}.nix-unit self.packages.${pkgs.system}.nix-unit
pythonWithDeps pythonWithDeps
]; ];
CLAN_FLAKE = self;
# This is required for the python tests, where some nix libs depend on nixpkgs
CLAN_NIXPKGS = pkgs.path;
# sets up an editable install and add enty points to $PATH # sets up an editable install and add enty points to $PATH
# This provides dummy options for testing clan config and prevents it from # This provides dummy options for testing clan config and prevents it from
# evaluating the flake .# # evaluating the flake .#
CLAN_OPTIONS_FILE = ./clan_cli/config/jsonschema/options.json; CLAN_OPTIONS_FILE = ./clan_cli/config/jsonschema/options.json;
PYTHONPATH = "${pythonWithDeps}/${pythonWithDeps.sitePackages}";
shellHook = '' shellHook = ''
tmp_path=$(realpath ./.direnv) tmp_path=$(realpath ./.direnv)
repo_root=$(realpath .)
mkdir -p "$tmp_path/python/${pythonWithDeps.sitePackages}" rm -f clan_cli/nixpkgs
ln -sf ${clan-cli.nixpkgs} clan_cli/nixpkgs
${pythonWithDeps.interpreter} -m pip install \
--quiet \
--disable-pip-version-check \
--no-index \
--no-build-isolation \
--prefix "$tmp_path/python" \
--editable $repo_root
export PATH="$tmp_path/bin:${checkScript}/bin:$PATH" export PATH="$tmp_path/bin:${checkScript}/bin:$PATH"
export PYTHONPATH="$repo_root:$tmp_path/python/${pythonWithDeps.sitePackages}:${pythonWithDeps}/${pythonWithDeps.sitePackages}" export PYTHONPATH="$PYTHONPATH:$(pwd)"
export XDG_DATA_DIRS="$tmp_path/share''${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}" export XDG_DATA_DIRS="$tmp_path/share''${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}"
export fish_complete_path="$tmp_path/share/fish/vendor_completions.d''${fish_complete_path:+:$fish_complete_path}" export fish_complete_path="$tmp_path/share/fish/vendor_completions.d''${fish_complete_path:+:$fish_complete_path}"
mkdir -p \ mkdir -p \

View File

@ -6,6 +6,8 @@ from typing import Generator
import pytest import pytest
from clan_cli.dirs import nixpkgs
sys.path.append(os.path.join(os.path.dirname(__file__), "helpers")) sys.path.append(os.path.join(os.path.dirname(__file__), "helpers"))
pytest_plugins = [ pytest_plugins = [
@ -27,15 +29,8 @@ def monkeymodule() -> Generator[pytest.MonkeyPatch, None, None]:
yield mp yield mp
# fixture for the example flake located under ./example_flake
# The flake is a template that is copied to a temporary location.
# Variables like __CLAN_NIXPKGS__ are replaced with the value of the
# CLAN_NIXPKGS environment variable.
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def machine_flake(monkeymodule: pytest.MonkeyPatch) -> Generator[Path, None, None]: def machine_flake(monkeymodule: pytest.MonkeyPatch) -> Generator[Path, None, None]:
CLAN_NIXPKGS = os.environ.get("CLAN_NIXPKGS", "")
if CLAN_NIXPKGS == "":
raise Exception("CLAN_NIXPKGS not set")
template = Path(__file__).parent / "machine_flake" template = Path(__file__).parent / "machine_flake"
# copy the template to a new temporary location # copy the template to a new temporary location
with tempfile.TemporaryDirectory() as tmpdir_: with tempfile.TemporaryDirectory() as tmpdir_:
@ -49,7 +44,7 @@ def machine_flake(monkeymodule: pytest.MonkeyPatch) -> Generator[Path, None, Non
# provided by get_clan_flake_toplevel # provided by get_clan_flake_toplevel
flake_nix = flake / "flake.nix" flake_nix = flake / "flake.nix"
flake_nix.write_text( flake_nix.write_text(
flake_nix.read_text().replace("__CLAN_NIXPKGS__", CLAN_NIXPKGS) flake_nix.read_text().replace("__NIXPKGS__", str(nixpkgs()))
) )
# check that an empty config is returned if no json file exists # check that an empty config is returned if no json file exists
monkeymodule.chdir(flake) monkeymodule.chdir(flake)

View File

@ -1,7 +1,7 @@
{ {
inputs = { inputs = {
# this placeholder is replaced by the path to nixpkgs # this placeholder is replaced by the path to nixpkgs
nixpkgs.url = "__CLAN_NIXPKGS__"; nixpkgs.url = "__NIXPKGS__";
}; };
outputs = _inputs: { outputs = _inputs: {

View File

@ -1,12 +1,13 @@
import os
import sys import sys
from typing import Union from typing import Union
import pytest import pytest
import pytest_subprocess.fake_process import pytest_subprocess.fake_process
from environment import mock_env
from pytest_subprocess import utils from pytest_subprocess import utils
import clan_cli import clan_cli
from clan_cli.dirs import flake_registry
from clan_cli.ssh import cli from clan_cli.ssh import cli
@ -21,56 +22,70 @@ def test_no_args(
# using fp fixture from pytest-subprocess # using fp fixture from pytest-subprocess
def test_ssh_no_pass(fp: pytest_subprocess.fake_process.FakeProcess) -> None: def test_ssh_no_pass(
with mock_env(CLAN_FLAKE="/mocked-flake"): fp: pytest_subprocess.fake_process.FakeProcess, monkeypatch: pytest.MonkeyPatch
host = "somehost" ) -> None:
user = "user" host = "somehost"
cmd: list[Union[str, utils.Any]] = [ user = "user"
"nix", if os.environ.get("IN_NIX_SANDBOX"):
"shell", monkeypatch.delenv("IN_NIX_SANDBOX")
"path:/mocked-flake#tor", cmd: list[Union[str, utils.Any]] = [
"path:/mocked-flake#openssh", "nix",
"-c", "shell",
"torify", "--extra-experimental-features",
"ssh", "nix-command flakes",
"-o", "--flake-registry",
"UserKnownHostsFile=/dev/null", str(flake_registry()),
"-o", "nixpkgs#tor",
"StrictHostKeyChecking=no", "nixpkgs#openssh",
f"{user}@{host}", "-c",
fp.any(), "torify",
] "ssh",
fp.register(cmd) "-o",
cli.ssh( "UserKnownHostsFile=/dev/null",
host=host, "-o",
user=user, "StrictHostKeyChecking=no",
) f"{user}@{host}",
assert fp.call_count(cmd) == 1 fp.any(),
]
fp.register(cmd)
cli.ssh(
host=host,
user=user,
)
assert fp.call_count(cmd) == 1
def test_ssh_with_pass(fp: pytest_subprocess.fake_process.FakeProcess) -> None: def test_ssh_with_pass(
with mock_env(CLAN_FLAKE="/mocked-flake"): fp: pytest_subprocess.fake_process.FakeProcess, monkeypatch: pytest.MonkeyPatch
host = "somehost" ) -> None:
user = "user" host = "somehost"
cmd: list[Union[str, utils.Any]] = [ user = "user"
"nix", if os.environ.get("IN_NIX_SANDBOX"):
"shell", monkeypatch.delenv("IN_NIX_SANDBOX")
"path:/mocked-flake#tor", cmd: list[Union[str, utils.Any]] = [
"path:/mocked-flake#openssh", "nix",
"path:/mocked-flake#sshpass", "shell",
"-c", "--extra-experimental-features",
"torify", "nix-command flakes",
"sshpass", "--flake-registry",
"-p", str(flake_registry()),
fp.any(), "nixpkgs#tor",
] "nixpkgs#openssh",
fp.register(cmd) "nixpkgs#sshpass",
cli.ssh( "-c",
host=host, "torify",
user=user, "sshpass",
password="XXX", "-p",
) fp.any(),
assert fp.call_count(cmd) == 1 ]
fp.register(cmd)
cli.ssh(
host=host,
user=user,
password="XXX",
)
assert fp.call_count(cmd) == 1
def test_qrcode_scan(fp: pytest_subprocess.fake_process.FakeProcess) -> None: def test_qrcode_scan(fp: pytest_subprocess.fake_process.FakeProcess) -> None: