From c14b7bbe2cc112e39435854d169518f119c16ca3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Domen=20Ko=C5=BEar?= Date: Thu, 27 Jul 2023 14:10:32 +0100 Subject: [PATCH] cli --- .envrc | 2 +- .github/workflows/buildtest.yml | 8 +- devenv.nix | 41 +---- examples/clickhouse/.test.sh | 6 +- src/devenv/cli.py | 249 ++++++++++++++++++++++++++----- src/devenv/log.py | 14 +- src/devenv/yaml.py | 12 +- src/modules/containers.nix | 7 +- src/modules/devenv-lib.nix | 27 ---- src/modules/flake.tmpl.nix | 9 +- src/modules/languages/php.nix | 6 +- src/modules/languages/python.nix | 6 +- src/modules/languages/ruby.nix | 6 +- src/modules/languages/rust.nix | 7 +- src/modules/lib.nix | 61 ++++++++ src/modules/tests.nix | 43 ++++++ src/modules/top-level.nix | 25 ++-- 17 files changed, 367 insertions(+), 162 deletions(-) delete mode 100644 src/modules/devenv-lib.nix create mode 100644 src/modules/lib.nix create mode 100644 src/modules/tests.nix diff --git a/.envrc b/.envrc index e207b3c3b..53b74fc41 100755 --- a/.envrc +++ b/.envrc @@ -6,4 +6,4 @@ set -euo pipefail # External users should use `source_url` to load this file source_env ./direnvrc -use devenv +use devenv \ No newline at end of file diff --git a/.github/workflows/buildtest.yml b/.github/workflows/buildtest.yml index 93bba6813..a9f5e1915 100644 --- a/.github/workflows/buildtest.yml +++ b/.github/workflows/buildtest.yml @@ -67,12 +67,12 @@ jobs: authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' - run: | nix profile remove '.*' - nix profile install --accept-flake-config . "nixpkgs#gawk" + nix profile install --accept-flake-config . - name: Disable package aliases run: | mkdir -p ~/.config/nixpkgs echo '{ allowAliases = false; }' > ~/.config/nixpkgs/config.nix - - run: devenv shell devenv-test-example ${{ matrix.example }} + - run: devenv test ${{ matrix.example }} direnv: name: direnv (${{ join(matrix.os) }}) needs: build @@ -92,7 +92,7 @@ jobs: authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' - run: | mv ./examples/simple/devenv.yaml ./examples/simple/devenv.yaml.orig - awk ' + nix run nixpkgs#gawk -- ' { print } /^inputs:$/ { print " devenv:"; @@ -100,7 +100,7 @@ jobs: } ' ./examples/simple/devenv.yaml.orig > ./examples/simple/devenv.yaml nix profile remove '.*' - nix profile install . 'nixpkgs#direnv' nix profile install --accept-flake-config + nix profile install . 'nixpkgs#direnv' mkdir -p ~/.config/direnv/ cat > ~/.config/direnv/direnv.toml << 'EOF' [global] diff --git a/devenv.nix b/devenv.nix index e8fb126b9..917b96ee5 100644 --- a/devenv.nix +++ b/devenv.nix @@ -1,9 +1,7 @@ { inputs, pkgs, lib, config, ... }: { - env = { - DEVENV_NIX = inputs.nix.packages.${pkgs.stdenv.system}.nix; - }; + env.DEVENV_NIX = inputs.nix.packages.${pkgs.stdenv.system}.nix; packages = [ pkgs.cairo @@ -82,43 +80,6 @@ popd rm -rf "$tmp" ''; - scripts.devenv-test-all-examples.exec = '' - for dir in $(ls examples); do - devenv-test-example $dir - done - ''; - scripts.devenv-test-example.exec = '' - # execute all trap_ function on exit - trap 'eval $(declare -F | grep -o "trap_[^ ]*" | tr "\n" ";")' EXIT - - set -e - example="$PWD/examples/$1" - pushd $example - mv devenv.yaml devenv.yaml.orig - awk ' - { print } - /^inputs:$/ { - print " devenv:"; - print " url: path:../../src/modules"; - } - ' devenv.yaml.orig > devenv.yaml - trap_restore_yaml() { - mv "$example/devenv.yaml.orig" "$example/devenv.yaml" - } - devenv ci - if [ -f .test.sh ]; then - trap_restore_local() { - rm "$example/devenv.local.nix" - rm -rf "$example/.devenv" - } - # coreutils-full provides timeout on darwin - echo "{ pkgs, ... }: { packages = [ pkgs.coreutils-full ]; }" > devenv.local.nix - devenv shell ./.test.sh - else - devenv shell ls - fi - popd - ''; scripts."devenv-generate-doc-options".exec = '' set -e options=$(nix build --impure --extra-experimental-features 'flakes nix-command' --show-trace --print-out-paths --no-link '.#devenv-docs-options') diff --git a/examples/clickhouse/.test.sh b/examples/clickhouse/.test.sh index b3e3c082c..542a8d0e9 100755 --- a/examples/clickhouse/.test.sh +++ b/examples/clickhouse/.test.sh @@ -1,6 +1,2 @@ -#!/bin/sh -set -ex -pkill clickhouse -devenv up& timeout 20 bash -c 'until echo > /dev/tcp/localhost/9000; do sleep 0.5; done' -clickhouse-client --query "SELECT 1" +clickhouse-client --query "SELECT 1" \ No newline at end of file diff --git a/src/devenv/cli.py b/src/devenv/cli.py index 6031ea449..20dc51a1a 100644 --- a/src/devenv/cli.py +++ b/src/devenv/cli.py @@ -1,8 +1,12 @@ import os +import shlex import shutil +import signal import subprocess +import tempfile import time import re +import sys from filelock import FileLock from contextlib import suppress from pathlib import Path @@ -20,7 +24,9 @@ NIX_FLAGS = [ "--show-trace", "--extra-experimental-features", - "\"nix-command flakes\"", + "nix-command", + "--extra-experimental-features", + "flakes", "--option", "warn-dirty", "false", @@ -38,14 +44,16 @@ # define system like x86_64-linux SYSTEM = os.uname().machine.lower().replace("arm", "aarch") + "-" + os.uname().sysname.lower() -def run_nix(command: str) -> str: +def run_nix(command: str, skip_exc_wrapping=False, replace_shell=False) -> str: ctx = click.get_current_context() nix_flags = ctx.obj['nix_flags'] flags = " ".join(NIX_FLAGS) + " " + " ".join(nix_flags) command_flags = " ".join(ctx.obj['command_flags']) - return run_command(f"nix {flags} {command} {command_flags}") + return run_command(f"nix {flags} {command} {command_flags}", + skip_exc_wrapping=skip_exc_wrapping, + replace_shell=replace_shell) -def run_command(command: str) -> str: +def run_command(command: str, disable_stderr=False, skip_exc_wrapping=False, replace_shell=False) -> str: if command.startswith("nix"): if os.environ.get("DEVENV_NIX"): nix = os.path.join(os.environ["DEVENV_NIX"], "bin") @@ -55,16 +63,24 @@ def run_command(command: str) -> str: log("Please follow https://devenv.sh/getting-started/ to install devenv.", level="error") exit(1) try: - return subprocess.run( - command, - shell=True, - check=True, - env=os.environ.copy(), - stdout=subprocess.PIPE, - universal_newlines=True).stdout.strip() + if click.get_current_context().obj['verbose']: + log(f"Running command: {command}", level="debug") + if replace_shell: + splitted_command = shlex.split(command.strip()) + os.execv(splitted_command[0], splitted_command) + else: + return subprocess.run( + command, + shell=True, + check=True, + env=os.environ.copy(), + stdout=subprocess.PIPE, + stdin=subprocess.PIPE, + stderr=None if not disable_stderr else subprocess.DEVNULL, + universal_newlines=True).stdout.strip() except subprocess.CalledProcessError as e: - if e.returncode == 130: - pass # we're exiting the shell + if skip_exc_wrapping: + exit(e.returncode) else: click.echo("\n", err=True) log(f"Following command exited with code {e.returncode}:\n\n {e.cmd}", level="error") @@ -73,6 +89,10 @@ def run_command(command: str) -> str: CONTEXT_SETTINGS = dict(max_content_width=120) @click.group(context_settings=CONTEXT_SETTINGS) +@click.option( + '--verbose', '-v', + help='Enable verbose output.', + is_flag=True) @click.option( '--nix-flags', '-n', help='Flags to pass to Nix. See `man nix.conf 5`. Example: --nix-flags "--option bash-prompt >"', @@ -95,10 +115,11 @@ def run_command(command: str) -> str: help='Disable Nix evaluation cache.', is_flag=True) @click.pass_context -def cli(ctx, disable_eval_cache, offline, system, debugger, nix_flags): +def cli(ctx, disable_eval_cache, offline, system, debugger, nix_flags, verbose): """https://devenv.sh: Fast, Declarative, Reproducible, and Composable Developer Environments.""" ctx.ensure_object(dict) ctx.obj['system'] = system + ctx.obj['verbose'] = verbose ctx.obj['command_flags'] = [] ctx.obj['nix_flags'] = list(nix_flags) ctx.obj['nix_flags'] += ['--system', system] @@ -120,6 +141,19 @@ def cli(ctx, disable_eval_cache, offline, system, debugger, nix_flags): Path(ctx.obj['gc_root']).mkdir(parents=True, exist_ok=True) ctx.obj['gc_project'] = os.path.join(ctx.obj['gc_root'], str(int(time.time() * 1000))) +@cli.group() +def processes(): + pass + + +DEVENV_DIR = Path(os.getcwd()) / '.devenv' +os.environ['DEVENV_DIR'] = str(DEVENV_DIR) +DEVENV_GC = DEVENV_DIR / 'gc' +os.environ['DEVENV_GC'] = str(DEVENV_GC) + +PROCESSES_PID = DEVENV_DIR / 'processes.pid' +PROCESSES_LOG = DEVENV_DIR / 'processes.log' + def add_gc(name, store_path): """Register a GC root""" @@ -136,10 +170,6 @@ def assemble(ctx): log(' $ devenv init', level="error") exit(1) - DEVENV_DIR = Path(os.getcwd()) / '.devenv' - os.environ['DEVENV_DIR'] = str(DEVENV_DIR) - DEVENV_GC = DEVENV_DIR / 'gc' - os.environ['DEVENV_GC'] = str(DEVENV_GC) DEVENV_GC.mkdir(parents=True, exist_ok=True) if os.path.exists('devenv.yaml'): @@ -206,16 +236,16 @@ def cleanup_symlinks(folder): to_gc.append(full_path) return to_gc, removed_symlinks -def get_dev_environment(ctx, is_shell=False): +def get_dev_environment(ctx, logging=True): ctx.invoke(assemble) - if is_shell: + if logging: action = log_task('Building shell') else: action = suppress() with action: gc_root = os.path.join(os.environ['DEVENV_GC'], 'shell') - env = run_nix(f"print-dev-env --impure --profile '{gc_root}'") - run_command(f"nix-env -p '{gc_root}' --delete-generations old") + env = run_nix(f"print-dev-env --profile '{gc_root}'") + run_command(f"nix-env -p '{gc_root}' --delete-generations old", disable_stderr=True) symlink_force(Path(f'{ctx.obj["gc_project"]}-shell'), gc_root) return env, gc_root @@ -232,12 +262,12 @@ def get_dev_environment(ctx, is_shell=False): @click.argument('cmd', required=False) @click.pass_context def shell(ctx, cmd, extra_args): - env, gc_root = get_dev_environment(ctx, is_shell=True) + env, gc_root = get_dev_environment(ctx) if cmd: - run_nix(f"develop '{gc_root}' -c {cmd} {' '.join(extra_args)}") + run_nix(f"develop '{gc_root}' -c {cmd} {' '.join(extra_args)}", replace_shell=True) else: log('Entering shell', level="info") - run_nix(f"develop '{gc_root}'") + run_nix(f"develop '{gc_root}'", replace_shell=True) def symlink_force(src, dst): # locking is needed until https://github.com/python/cpython/pull/14464 @@ -250,23 +280,66 @@ def symlink_force(src, dst): short_help="Starts processes in foreground. See http://devenv.sh/processes", ) @click.argument('command', required=False) +@click.option('--detach', '-d', is_flag=True, help="Starts processes in the background.") @click.pass_context -def up(ctx, command): +def up(ctx, command, detach): with log_task('Building processes'): ctx.invoke(assemble) - procfilescript = run_nix(f"build --no-link --print-out-paths --impure '.#procfileScript'") + procfilescript = run_nix(f"build --no-link --print-out-paths '.#procfileScript'") with open(procfilescript, 'r') as file: contents = file.read().strip() if contents == '': log("No 'processes' option defined: https://devenv.sh/processes/", level="error") exit(1) else: + env, _ = get_dev_environment(ctx) log('Starting processes ...', level="info") add_gc('procfilescript', procfilescript) - # TODO: print output to stdout - #run_command(procfilescript + ' ' + (command or '')) - args = [] if not command else [command] - subprocess.run([procfilescript] + args) + processes_script = os.path.join(DEVENV_DIR, 'processes') + with open(processes_script, 'w') as file: + file.write(f""" +${env} +{procfilescript} {command or ""} + """) + os.chmod(processes_script, 0o755) + + if detach: + process = subprocess.Popen( + [processes_script], + stdout=open(PROCESSES_LOG, 'w'), + stderr=subprocess.STDOUT, + ) + + with open(PROCESSES_PID, 'w') as file: + file.write(str(process.pid)) + log(f" PID is {process.pid}.", level="info") + log(f" See logs: $ tail -f {PROCESSES_LOG}", level="info") + log(f" Stop: $ devenv processes stop", level="info") + else: + os.execv(processes_script, [processes_script]) + +processes.add_command(up) + +@processes.command( + help="Stops processes started with 'devenv up'.", + short_help="Stops processes started with 'devenv up'.", +) +def stop(): + with log_task('Stopping processes', newline=False): + if not os.path.exists(PROCESSES_PID): + log("No processes running.", level="error") + exit(1) + + with open(PROCESSES_PID, 'r') as file: + pid = int(file.read()) + + try: + os.kill(pid, signal.SIGTERM) + except ProcessLookupError: + log(f"Process with PID {pid} not found.", level="error") + exit(1) + + os.remove(PROCESSES_PID) @cli.command() @click.argument('name') @@ -274,7 +347,7 @@ def up(ctx, command): def search(ctx, name): """Search packages matching NAME in nixpkgs input.""" ctx.invoke(assemble) - options = run_nix(f"build --no-link --print-out-paths '.#optionsJSON' --impure") + options = run_nix(f"build --no-link --print-out-paths '.#optionsJSON' ") search = run_nix(f"search --json nixpkgs {name}") with open(Path(options) / 'share' / 'doc' / 'nixos' / 'options.json') as f: @@ -292,7 +365,7 @@ def search(ctx, name): search_results = [] for key, value in json.loads(search).items(): search_results.append( - (".".join(key.split('.')[2:]) + ("pkgs." + (".".join(key.split('.')[2:])) , value['version'] , value['description'][:80] ) @@ -339,6 +412,7 @@ def container(ctx, registry, copy, copy_args, docker_run, container_name): # copy container if copy or docker_run: with log_task(f'Copying {container_name} container'): + # we need --impure here for DEVENV_CONTAINER copy_script = run_nix(f"build --print-out-paths --no-link \ --impure .#devenv.containers.\"{container_name}\".copyScript") @@ -351,8 +425,9 @@ def container(ctx, registry, copy, copy_args, docker_run, container_name): check=True) if docker_run: - with log_task(f'Starting {container_name} container'): - docker_script = run_nix(f"build --print-out-paths --no-link --impure \ + log(f'Starting {container_name} container', level="info") + # we need --impure here for DEVENV_CONTAINER + docker_script = run_nix(f"build --print-out-paths --no-link --impure \ .#devenv.containers.\"{container_name}\".dockerRun") subprocess.run(docker_script) @@ -371,7 +446,7 @@ def info(ctx): inputs = matches.group(1) else: inputs = "" - info_ = run_nix("eval --raw '.#info' --impure") + info_ = run_nix("eval --raw '.#info'") click.echo(f"{inputs}\n{info_}") @cli.command() @@ -449,10 +524,10 @@ def ci(ctx): output_path = run_nix(f"build --no-link --print-out-paths --impure .#ci") add_gc('ci', output_path) -@cli.command() +@cli.command(hidden=True) @click.pass_context def print_dev_env(ctx): - env, _ = get_dev_environment(ctx) + env, _ = get_dev_environment(ctx, logging=False) click.echo(env) def get_version(): @@ -489,4 +564,100 @@ def add(ctx, name, url, follows): attrs['inputs'] = inputs devenv['inputs'][name] = attrs - write_yaml(devenv) \ No newline at end of file + write_yaml(devenv) + +@cli.command( + help="Run tests. See http://devenv.sh/tests/", + short_help="Run tests. See http://devenv.sh/tests/", +) +@click.argument('names', nargs=-1) +@click.option('--debug', is_flag=True, help='Run tests in debug mode.') +@click.pass_context +def test(ctx, debug, names): + ctx.invoke(assemble) + with log_task(f"Gathering tests", newline=False): + tests = json.loads(run_nix(f"eval .#devenv.tests --json")) + + if not names: + names = "local" + + # group tests by tags + tags = {} + for name, test in tests.items(): + for tag in test['tags']: + if tag not in tags: + tags[tag] = [] + tags[tag].append(name) + + selected_tests = [] + for name in names: + if name in tests: + selected_tests.append(name) + tag_tests = tags.get(name, {}) + if tag_tests: + selected_tests.extend(tag_tests) + + log(f"Found {len(selected_tests)} tests:", level="info") + + pwd = os.getcwd() + + for name in selected_tests: + with log_task(f" Running {name}"): + with tempfile.TemporaryDirectory(prefix=name + "_") as tmpdir: + os.chdir(tmpdir) + test = tests[name] + + write_if_defined("devenv.nix", test.get('nix')) + write_if_defined("devenv.yaml", test.get('yaml')) + write_if_defined(".test.sh", test.get('test')) + if os.path.exists(".test.sh"): + os.chmod(".test.sh", 0o755) + write_if_defined("devenv.local.nix", """ + { pkgs, ... }: { + packages = [ pkgs.coreutils-full ]; + } + """) + + # plug in devenv input if needed + if os.path.exists(os.path.join(pwd, 'src/modules/latest-version')): + log(" Detected devenv module. Using src/modules for tests.", level="info") + + modules = os.path.join(pwd, 'src/modules') + yaml = read_yaml() + yaml['inputs']['devenv'] = {'url': f'path:{modules}'} + write_yaml(yaml) + + devenv = sys.argv[0] + has_processes = False + try: + run_command(f"{devenv} ci") + + has_processes = os.path.exists(".devenv/gc/ci") and "-devenv-up" in run_command("cat .devenv/gc/ci") + + if has_processes: + run_command(f"{devenv} up -d") + # TODO doesn't log :( + run_command("tail -f .devenv/processes.log&") + try: + if os.path.exists(".test.sh"): + run_command("devenv shell bash ./.test.sh") + finally: + if has_processes and not debug: + run_command(f"{devenv} processes stop") + except BaseException as e: + log(f"Test {name} failed.", level="error") + if debug: + log(f"Entering shell because of the --debug flag:", level="warning") + log(f" - devenv: {devenv}", level="warning") + log(f" - cwd: {tmpdir}", level="warning") + if has_processes: + log(f" - up logs: .devenv/processes.log:", level="warning") + sys.ps1 = f"{sys.ps1} (devenv {name})" + os.execv("/bin/sh", ["/bin/sh"]) + else: + raise e + +def write_if_defined(file, content): + if content: + with open(file, 'w') as f: + f.write(content) \ No newline at end of file diff --git a/src/devenv/log.py b/src/devenv/log.py index 04709e3f4..4d25a5233 100644 --- a/src/devenv/log.py +++ b/src/devenv/log.py @@ -1,25 +1,29 @@ from typing import Literal +import time import click class log_task: """Context manager for logging progress of a task.""" - def __init__(self, message,): + def __init__(self, message, newline=True): self.message = message + self.newline = newline def __enter__(self): prefix = click.style("•", fg="blue") - click.echo(f"{prefix} {self.message} ...", nl=False) + self.start = time.time() + click.echo(f"{prefix} {self.message} ...", nl=self.newline) def __exit__(self, exc, *args): + end = time.time() if exc: prefix = click.style("✖", fg="red") else: prefix = click.style("✔", fg="green") - click.echo(f"\r{prefix} {self.message}") + click.echo(f"\r{prefix} {self.message} in {end - self.start:.1f}s.") -LogLevel = Literal["info", "warning", "error"] +LogLevel = Literal["info", "warning", "error", "debug"] def log(message, level: LogLevel): match level: @@ -29,3 +33,5 @@ def log(message, level: LogLevel): click.echo(click.style("• ", fg="yellow") + message, err=True) case "error": click.echo(click.style("✖ ", fg="red") + message, err=True) + case "debug": + click.echo(click.style("• ", fg="magenta") + message, err=True) diff --git a/src/devenv/yaml.py b/src/devenv/yaml.py index 434f340fc..e3529b9b3 100644 --- a/src/devenv/yaml.py +++ b/src/devenv/yaml.py @@ -24,12 +24,12 @@ YAML_FILE = Path("devenv.yaml") def read_yaml(): - try: - with open(YAML_FILE) as f: - return load(f.read(), schema, label="devenv.yaml") - except YAMLError as error: - print("Validation error in `devenv.yaml`", error) - sys.exit(1) + try: + with open(YAML_FILE) as f: + return load(f.read(), schema, label="devenv.yaml") + except YAMLError as error: + print("Validation error in `devenv.yaml`", error) + sys.exit(1) def write_yaml(yaml): with open(YAML_FILE, "w") as f: diff --git a/src/modules/containers.nix b/src/modules/containers.nix index e7e0e8e0a..9e31eaae0 100644 --- a/src/modules/containers.nix +++ b/src/modules/containers.nix @@ -1,4 +1,4 @@ -{ pkgs, config, lib, inputs, self, ... }: +{ pkgs, config, lib, self, ... }: let projectName = name: @@ -7,16 +7,15 @@ let else config.name; types = lib.types; envContainerName = builtins.getEnv "DEVENV_CONTAINER"; - devenvlib = import ./devenv-lib.nix { inherit pkgs config inputs lib; }; - nix2containerInput = devenvlib.getInput { + nix2containerInput = config.lib.getInput { name = "nix2container"; url = "github:nlewo/nix2container"; attribute = "containers"; follows = [ "nixpkgs" ]; }; nix2container = nix2containerInput.packages.${pkgs.stdenv.system}; - mk-shell-bin = devenvlib.getInput { + mk-shell-bin = config.lib.getInput { name = "mk-shell-bin"; url = "github:rrbutani/nix-mk-shell-bin"; attribute = "containers"; diff --git a/src/modules/devenv-lib.nix b/src/modules/devenv-lib.nix deleted file mode 100644 index 0dee070f5..000000000 --- a/src/modules/devenv-lib.nix +++ /dev/null @@ -1,27 +0,0 @@ -{ pkgs, lib, config, inputs }: - -{ - getInput = { name, url, attribute, follows ? [ ] }: - let - flags = lib.concatStringsSep " " (map (i: "--follows ${i}") follows); - yaml_follows = lib.concatStringsSep "\n " (map (i: "${i}:\n follows: ${i}") follows); - command = - if lib.versionAtLeast config.devenv.cliVersion "1.0" - then '' - run the following command: - - $ devenv inputs add ${name} ${url} ${flags} - '' - else '' - add the following to your devenv.yaml: - - ✨ devenv 1.0 made this easier: https://devenv.sh/getting-started/#installation ✨ - - inputs: - ${name}: - url: ${url} - ${if follows != [] then "inputs:\n ${yaml_follows}" else ""} - ''; - in - inputs.${name} or (throw "To use '${attribute}', ${command}\n\n"); -} diff --git a/src/modules/flake.tmpl.nix b/src/modules/flake.tmpl.nix index cbb8ac88c..de839cb91 100644 --- a/src/modules/flake.tmpl.nix +++ b/src/modules/flake.tmpl.nix @@ -55,7 +55,10 @@ specialArgs = inputs // { inherit inputs pkgs; }; modules = [ (inputs.devenv.modules + /top-level.nix) - { devenv.cliVersion = "${version}"; } + { + devenv.cliVersion = "${version}"; + devenv.root = devenv_root; + } ] ++ (map importModule (devenv.imports or [ ])) ++ [ ./devenv.nix (devenv.devenv or { }) @@ -85,7 +88,9 @@ inherit (config) info procfileScript procfileEnv procfile; ci = config.ciDerivation; }; - devenv.containers = config.containers; + devenv = { + inherit (config) containers tests; + }; devShell."${system}" = config.shell; }; } diff --git a/src/modules/languages/php.nix b/src/modules/languages/php.nix index 8098d37a9..f8303fea3 100644 --- a/src/modules/languages/php.nix +++ b/src/modules/languages/php.nix @@ -1,4 +1,4 @@ -{ pkgs, config, lib, inputs, ... }: +{ pkgs, config, lib, ... }: with lib; @@ -7,9 +7,7 @@ let cfg = config.languages.php; - devenvlib = import ../devenv-lib.nix { inherit pkgs config inputs lib; }; - - phps = devenvlib.getInput { + phps = config.lib.getInput { name = "phps"; url = "github:fossar/nix-phps"; attribute = "languages.php.version"; diff --git a/src/modules/languages/python.nix b/src/modules/languages/python.nix index b4d921d28..9e4cd4b01 100644 --- a/src/modules/languages/python.nix +++ b/src/modules/languages/python.nix @@ -1,11 +1,9 @@ -{ pkgs, config, lib, inputs, ... }: +{ pkgs, config, lib, ... }: let cfg = config.languages.python; - devenvlib = import ../devenv-lib.nix { inherit pkgs config inputs lib; }; - - nixpkgs-python = devenvlib.getInput { + nixpkgs-python = config.lib.getInput { name = "nixpkgs-python"; url = "github:cachix/nixpkgs-python"; attribute = "languages.python.version"; diff --git a/src/modules/languages/ruby.nix b/src/modules/languages/ruby.nix index 96ed78e3b..36a95a4ad 100644 --- a/src/modules/languages/ruby.nix +++ b/src/modules/languages/ruby.nix @@ -1,11 +1,9 @@ -{ pkgs, config, lib, inputs, ... }: +{ pkgs, config, lib, ... }: let cfg = config.languages.ruby; - devenvlib = import ../devenv-lib.nix { inherit pkgs config inputs lib; }; - - nixpkgs-ruby = devenvlib.getInput { + nixpkgs-ruby = config.lib.getInput { name = "nixpkgs-ruby"; url = "github:bobvanderlinden/nixpkgs-ruby"; attribute = "languages.ruby.version or languages.ruby.versionFile"; diff --git a/src/modules/languages/rust.nix b/src/modules/languages/rust.nix index b9a3de236..3da71e1f8 100644 --- a/src/modules/languages/rust.nix +++ b/src/modules/languages/rust.nix @@ -1,17 +1,14 @@ -{ pkgs, config, lib, inputs, ... }: +{ pkgs, config, lib, ... }: let cfg = config.languages.rust; - devenvlib = import ../devenv-lib.nix { inherit pkgs config inputs lib; }; - - fenix = devenvlib.getInput { + fenix = config.lib.getInput { name = "fenix"; url = "github:nix-community/fenix"; attribute = "languages.rust.version"; follows = [ "nixpkgs" ]; }; - in { imports = [ diff --git a/src/modules/lib.nix b/src/modules/lib.nix new file mode 100644 index 000000000..5faaef041 --- /dev/null +++ b/src/modules/lib.nix @@ -0,0 +1,61 @@ +{ lib, config, inputs, ... }: + +{ + # freestyle + options.lib = lib.mkOption { + type = lib.types.attrsOf lib.types.anything; + internal = true; + }; + + config.lib = rec { + getInput = { name, url, attribute, follows ? [ ] }: + let + flags = lib.concatStringsSep " " (map (i: "--follows ${i}") follows); + yaml_follows = lib.concatStringsSep "\n " (map (i: "${i}:\n follows: ${i}") follows); + command = + if lib.versionAtLeast config.devenv.cliVersion "1.0" + then '' + run the following command: + + $ devenv inputs add ${name} ${url} ${flags} + '' + else '' + add the following to your devenv.yaml: + + ✨ devenv 1.0 made this easier: https://devenv.sh/getting-started/#installation ✨ + + inputs: + ${name}: + url: ${url} + ${if follows != [] then "inputs:\n ${yaml_follows}" else ""} + ''; + in + inputs.${name} or (throw "To use '${attribute}', ${command}\n\n"); + + mkTest = tags: dir: + { + inherit tags; + nix = builtins.readFile (dir + "/devenv.nix"); + yaml = + let + yaml = dir + "/devenv.yaml"; + in + if builtins.pathExists yaml + then builtins.readFile yaml + else null; + test = + let + test = dir + "/.test.sh"; + in + if builtins.pathExists test + then builtins.readFile test + else null; + }; + + mkTests = tags: folder: + let + mk = dir: mkTest tags (folder + "/${dir}"); + in + lib.genAttrs (builtins.attrNames (builtins.readDir folder)) mk; + }; +} diff --git a/src/modules/tests.nix b/src/modules/tests.nix new file mode 100644 index 000000000..a696610ae --- /dev/null +++ b/src/modules/tests.nix @@ -0,0 +1,43 @@ +{ config, pkgs, lib, ... }: + +let + testType = lib.types.submodule ({ config, ... }: { + options = { + nix = lib.mkOption { + type = lib.types.str; + example = "{ pkgs, ... }: {}"; + description = "devenv.nix code."; + }; + + tags = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ "local" ]; + description = "Tags for this test."; + }; + + yaml = lib.mkOption { + type = lib.types.nullOr lib.types.str; + example = '' + inputs: + nixpkgs: + url: github:NixOS/nixpkgs/nixpkgs-unstable + ''; + description = "devenv.yaml code."; + }; + + test = lib.mkOption { + type = lib.types.nullOr lib.types.str; + description = "Bash to be executed."; + }; + }; + }); +in +{ + options.tests = lib.mkOption { + type = lib.types.attrsOf testType; + default = { }; + description = "Tests for this module."; + }; + + config.tests = config.lib.mkTests [ "devenv" ] ./../../examples; +} diff --git a/src/modules/top-level.nix b/src/modules/top-level.nix index f884a504b..ab1fa1b15 100644 --- a/src/modules/top-level.nix +++ b/src/modules/top-level.nix @@ -110,6 +110,7 @@ in root = lib.mkOption { type = types.str; internal = true; + default = builtins.getEnv "PWD"; }; dotfile = lib.mkOption { @@ -137,6 +138,8 @@ in ./update-check.nix ./containers.nix ./debug.nix + ./lib.nix + ./tests.nix ] ++ (listEntries ./languages) ++ (listEntries ./services) @@ -145,20 +148,16 @@ in ; config = { - # TODO: figure out how to get relative path without impure mode - devenv.root = lib.mkDefault ( - let - pwd = builtins.getEnv "PWD"; - in - if pwd == "" then - throw '' + assertions = [ + { + assertion = config.devenv.root != ""; + message = '' devenv was not able to determine the current directory. - Make sure Nix runs with the `--impure` flag. - See https://devenv.sh/guides/using-with-flakes/ - '' - else pwd - ); + See https://devenv.sh/guides/using-with-flakes/ how to use it with flakes. + ''; + } + ]; devenv.dotfile = config.devenv.root + "/.devenv"; devenv.state = config.devenv.dotfile + "/state"; devenv.profile = profile; @@ -208,6 +207,6 @@ in infoSections."packages" = builtins.map (package: package.name) (builtins.filter (package: !(builtins.elem package.name (builtins.attrNames config.scripts))) config.packages); ci = [ config.shell.inputDerivation ]; - ciDerivation = pkgs.runCommand "ci" { } ("ls " + toString config.ci + " && touch $out"); + ciDerivation = pkgs.runCommand "ci" { } "echo ${toString config.ci} > $out"; }; }