diff --git a/devenv.nix b/devenv.nix index 2a133f77e..68165e806 100644 --- a/devenv.nix +++ b/devenv.nix @@ -72,7 +72,7 @@ grep -F 'nix-develop started succesfully' <./console grep -F "$(${lib.getExe pkgs.hello})" <./console # Test that a container can be built - nix build --impure .#container-processes + nix build --impure .#devenv-default-container-processes-spec popd rm -rf "$tmp" diff --git a/flake-module.nix b/flake-module.nix index b10eafdd4..1231abde5 100644 --- a/flake-module.nix +++ b/flake-module.nix @@ -10,8 +10,6 @@ devenvFlake: { flake-parts-lib, lib, inputs, ... }: { }; }]; }).type; - - shellPrefix = shellName: if shellName == "default" then "" else "${shellName}-"; in { @@ -44,8 +42,55 @@ devenvFlake: { flake-parts-lib, lib, inputs, ... }: { (shellName: devenv: lib.concatMapAttrs (containerName: container: - { "${shellPrefix shellName}container-${containerName}" = container.derivation; } - ) + let prefix = "devenv-${shellName}-container-${containerName}"; in { + "${prefix}-spec" = container.derivation; + }) + devenv.containers + ) + config.devenv.shells; + + config.apps = + lib.concatMapAttrs + (shellName: devenv: + lib.concatMapAttrs + (containerName: config: + let prefix = "devenv-${shellName}-container-${containerName}"; in { + "${prefix}-copy-to" = { + type = "app"; + program = pkgs.writeShellApplication { + name = "${prefix}-copy-to"; + text = '' + ${config.copyScript} ${config.derivation} "$@" + ''; + }; + }; + "${prefix}-docker-run" = { + type = "app"; + program = "${config.dockerRun}"; + }; + "${prefix}-docker-load" = { + type = "app"; + program = pkgs.writeShellApplication { + name = "${prefix}-docker-load"; + text = '' + ${config.copyScript} ${config.derivation} --registry local-docker "$@" + ''; + }; + }; + "${prefix}-podman-run" = { + type = "app"; + program = "${config.podmanRun}"; + }; + "${prefix}-podman-load" = { + type = "app"; + program = pkgs.writeShellApplication { + name = "${prefix}-podman-load"; + text = '' + ${config.copyScript} ${config.derivation} --registry local "$@" + ''; + }; + }; + }) devenv.containers ) config.devenv.shells; diff --git a/src/devenv-devShell.nix b/src/devenv-devShell.nix index c5aa16635..2bef932f4 100644 --- a/src/devenv-devShell.nix +++ b/src/devenv-devShell.nix @@ -1,46 +1,17 @@ { config, pkgs }: let lib = pkgs.lib; - version = lib.fileContents ./modules/latest-version; -in -pkgs.writeScriptBin "devenv" '' - #!/usr/bin/env bash - - # we want subshells to fail the program - set -e - NIX_FLAGS="--show-trace --extra-experimental-features nix-command --extra-experimental-features flakes" + app = pkgs.writeShellApplication { + name = "devenv-flake-cli"; + runtimeInputs = with pkgs; [ docopts ]; + text = builtins.readFile ./devenv-devShell.sh; + }; - command=$1 - if [[ ! -z $command ]]; then - shift - fi - - case $command in - up) - procfilescript=${config.procfileScript} - if [ "$(cat $procfilescript|tail -n +2)" = "" ]; then - echo "No 'processes' option defined: https://devenv.sh/processes/" - exit 1 - else - exec $procfilescript "$@" - fi - ;; - version) - echo "devenv: ${version}" - ;; - *) - echo "https://devenv.sh (version ${version}): Fast, Declarative, Reproducible, and Composable Developer Environments" - echo - echo "This is a flake integration wrapper that comes with a subset of functionality from the flakeless devenv CLI." - echo - echo "Usage: devenv command" - echo - echo "Commands:" - echo - echo "up Starts processes in foreground. See http://devenv.sh/processes" - echo "version Display devenv version" - echo - exit 1 - esac -'' + envs = lib.concatStringsSep " " (lib.mapAttrsToList lib.toShellVar { + PROCFILESCRIPT = config.procfileScript; + VERSION = lib.fileContents ./modules/latest-version; + CUSTOM_NIX_BIN = "${pkgs.nix}/bin"; + }); +in +pkgs.writeScriptBin "devenv" ''${envs} "${app}/bin/devenv-flake-cli" "$@"'' diff --git a/src/devenv-devShell.sh b/src/devenv-devShell.sh new file mode 100755 index 000000000..90cd9dc4f --- /dev/null +++ b/src/devenv-devShell.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +set -eEuo pipefail + +if [ "${DEBUG:-}" == 1 ]; then + set -x +fi +PROCFILESCRIPT="${PROCFILESCRIPT:-"placeholder"}" +VERSION="${VERSION:-"placeholder"}" +CUSTOM_NIX_BIN="${CUSTOM_NIX_BIN:-"$(dirname "$(command -v nix)")"}" + +NIX_FLAGS=(--show-trace --extra-experimental-features 'nix-command flakes') + +function nix { + "$CUSTOM_NIX_BIN/nix" "${NIX_FLAGS[@]}" "${@}" +} + +function container { + # shellcheck disable=SC1090 + source "$(command -v docopts).sh" + # shellcheck disable=SC2016 + eval "$(docopts -A args -h ' +Usage: container [options] [--] [...] + +Options: + -s , --shell `devenv.shells.` to use. [default: default] + -r , --registry Registry to copy the container to. + Available shortcuts: config, local-docker, local [default: config] + -i , --image Image name:tag to replace ${container.name}:${container.version} with. + --copy Copy the container to the registry. + --copy-args Arguments passed to `skopeo copy`. + --docker-run Execute `docker run`. + --podman-run Execute `podman run`. +' : "$@")" + + local run_args=() + eval "$(docopt_get_eval_array args '' run_args)" + + local registry="${args['--registry']}" + local copy_args=() + + local flake_root="${DEVENV_ROOT:-"${DIRENV_ROOT}"}" + local app_prefix="${flake_root}#devenv-${args['--shell']}-container-${args['']}" + + if [[ "${args['--copy']}" != false || "${args['--docker-run']}" != false || "${args['--podman-run']}" != false ]]; then + if [[ ${args['--docker-run']} == true ]]; then + registry=local-docker + elif [[ ${args['--podman-run']} == true ]]; then + registry=local + fi + if [[ -n "${args['--image']}" ]]; then + copy_args+=(--image "${args['--image']}") + fi + # shellcheck disable=SC2086 + nix run --impure "${app_prefix}-copy-to" -- --registry "${registry}" "${copy_args[@]}" ${args['--copy-args']} + fi + + if [[ "${args['--docker-run']}" != false ]]; then + nix run --impure "${app_prefix}-docker-run" -- "${run_args[@]}" + elif [[ "${args['--podman-run']}" != false ]]; then + nix run --impure "${app_prefix}-podman-run" -- "${run_args[@]}" + fi +} + +command="${1:-}" +if [[ -n "$command" ]]; then + shift +fi + +case "$command" in +up) + if [ "$(tail -n +2 <<<"$PROCFILESCRIPT")" = "" ]; then + echo "No 'processes' option defined: https://devenv.sh/processes/" + exit 1 + else + exec "$PROCFILESCRIPT" "$@" + fi + ;; +container) + container "$@" + ;; +version) + echo "devenv: ${VERSION}" + ;; +*) + echo "https://devenv.sh (version ${VERSION}): Fast, Declarative, Reproducible, and Composable Developer Environments" + echo + echo "This is a flake integration wrapper that comes with a subset of functionality from the flakeless devenv CLI." + echo + echo "Usage: devenv command" + echo + echo "Commands:" + echo + echo "up Starts processes in foreground. See http://devenv.sh/processes" + echo "version Display devenv version" + echo + exit 1 + ;; +esac diff --git a/src/devenv.nix b/src/devenv.nix index 0dc89dd2d..e0861c32f 100644 --- a/src/devenv.nix +++ b/src/devenv.nix @@ -112,17 +112,23 @@ pkgs.writeScriptBin "devenv" '' container) assemble help=$(${coreutils}/bin/cat << 'EOF' - Usage: container [options] CONTAINER-NAME + Usage: container [options] CONTAINER-NAME [--] [...] Options: - --registry= Registry to copy the container to. - --copy Copy the container to the registry. - --copy-args= Arguments passed to `skopeo copy`. - --docker-run Execute `docker run`. + -r , --registry Registry to copy the container to. + -i , --image Image name:tag to replace ''${container.name}:''${container.version} with. + --copy Copy the container to the registry. + --copy-args= Arguments passed to `skopeo copy`. + --docker-run Execute `docker run`. + --podman-run Execute `podman run`. EOF ) + # shellcheck disable=SC1090 + source "$(command -v ${docopts}/bin/docopts.sh)" eval "$(${docopts}/bin/docopts -A subcommand -h "$help" : "$@")" + local run_args=() + eval "$(docopt_get_eval_array args '' run_args)" export DEVENV_CONTAINER=1 container="''${subcommand[CONTAINER-NAME]}" @@ -132,20 +138,30 @@ pkgs.writeScriptBin "devenv" '' echo $spec # copy container - if [[ ''${subcommand[--copy]} != false || ''${subcommand[--docker-run]} != false ]]; then + if [[ ''${subcommand[--copy]} != false || ''${subcommand[--docker-run]} != false || ''${subcommand[--podman-run]} != false ]]; then copyScript=$(${nix}/bin/nix $NIX_FLAGS build --print-out-paths --no-link --impure ".#devenv.containers.\"$container\".copyScript") + args=() + if [[ ''${subcommand[--docker-run]} == true ]]; then - registry=docker-daemon: + registry=local-docker + elif [[ ''${subcommand[--podman-run]} == true ]]; then + registry=local else registry="''${subcommand[--registry]}" fi - $copyScript $spec $registry ''${subcommand[--copy-args]} + if [[ -n "''${subcommand[--image]}" ]]; then + args+=(--image "''${subcommand[--image]}") + fi + + $copyScript "$spec" --registry "$registry" "''${args[@]}" ''${subcommand[--copy-args]} fi # docker run if [[ ''${subcommand[--docker-run]} != false ]]; then - $(${nix}/bin/nix $NIX_FLAGS build --print-out-paths --no-link --impure ".#devenv.containers.\"$container\".dockerRun") + $(${nix}/bin/nix $NIX_FLAGS build --print-out-paths --no-link --impure ".#devenv.containers.\"$container\".dockerRun") -- "''${run_args[@]}" + elif [[ ''${subcommand[--podman-run]} != false ]]; then + $(${nix}/bin/nix $NIX_FLAGS build --print-out-paths --no-link --impure ".#devenv.containers.\"$container\".podmanRun") -- "''${run_args[@]}" fi ;; search) diff --git a/src/modules/containers.nix b/src/modules/containers.nix index a1b317ae2..e96970a29 100644 --- a/src/modules/containers.nix +++ b/src/modules/containers.nix @@ -1,6 +1,7 @@ { pkgs, config, lib, inputs, self, ... }: let + inherit (pkgs) docopts; projectName = name: if config.name == null then throw ''You need to set `name = "myproject";` or `containers.${name}.name = "mycontainer"; to be able to generate a container.'' @@ -32,54 +33,78 @@ let exec "$@" ''; - mkDerivation = cfg: nix2container.nix2container.buildImage { - name = cfg.name; - tag = cfg.version; - copyToRoot = [ - (pkgs.runCommand "create-paths" { } '' - mkdir -p $out/tmp - '') - (pkgs.buildEnv { - name = "root"; - paths = [ - pkgs.coreutils-full - pkgs.bash - ] ++ lib.optionals (cfg.copyToRoot != null) [ cfg.copyToRoot ]; - pathsToLink = "/"; - }) - ]; - config = { - Env = lib.mapAttrsToList (name: value: "${name}=${lib.escapeShellArg (toString value)}") containerEnv; - Cmd = [ cfg.startupCommand ]; - Entrypoint = cfg.entrypoint; - }; - }; + mkDerivation = cfg: nix2container.nix2container.buildImage ( + lib.attrsets.recursiveUpdate + { + name = cfg.name; + tag = cfg.version; + copyToRoot = [ + (pkgs.runCommand "create-paths" { } '' + mkdir -p $out/tmp $out/usr + ln -sfT /bin $out/usr/bin + '') + (pkgs.buildEnv { + name = "root"; + paths = [ + pkgs.coreutils-full + pkgs.bash + pkgs.dockerTools.caCertificates + (pkgs.writeShellApplication { + name = "devenv-entrypoint"; + text = ''exec ${lib.escapeShellArgs cfg.entrypoint} "$@"''; + }) + ] ++ lib.optionals (cfg.copyToRoot != null) [ cfg.copyToRoot ]; + pathsToLink = "/"; + }) + ]; + config = { + Env = lib.mapAttrsToList (name: value: "${name}=${lib.escapeShellArg (toString value)}") containerEnv; + Entrypoint = cfg.entrypoint; + Cmd = cfg.startupCommand; + } // (cfg.rawBuildConfig.config or { }); + } + cfg.rawBuildConfig + ); # mkCopyScript = cfg: pkgs.writeScript "copy-container" '' - container=$1 - shift + source "$(command -v ${docopts}/bin/docopts).sh" + eval "$(${docopts}/bin/docopts -A args -h ' + Usage: copy-container [options] [...] - if [[ "$1" == false ]]; then - registry=${cfg.registry} - else - registry="$1" - fi - shift + Options: + -r , --registry Registry to copy the container to, eg: docker://ghcr.io/ + Available shortcuts: config, local-docker, local [default: config] + -i , --image Image name and tag to use [default: ${cfg.name}:${cfg.version}] + ' : "$@")" + spec="''${args['']}" + + case "''${args['--registry']}" in + false|config) + registry="${cfg.registry}" + ;; + local-docker) + registry="docker-daemon:" + ;; + local|local-podman|local-containers|local-buildah) + registry="containers-storage:" + ;; + *) registry="$1" ;; + esac - dest="''${registry}${cfg.name}:${cfg.version}" + dest="''${registry}''${args['--image']}" - if [[ $# == 0 ]]; then - args=(${toString cfg.defaultCopyArgs}) + if [[ ''${args[',#']} == 0 ]]; then + argv=(${toString cfg.defaultCopyArgs}) else - args=("$@") + eval "$(docopt_get_eval_array args '' argv)" fi echo - echo "Copying container $container to $dest" + echo "Copying container $spec to $dest" echo - ${nix2container.skopeo-nix2container}/bin/skopeo --insecure-policy copy "nix:$container" "$dest" "''${args[@]}" + ${nix2container.skopeo-nix2container}/bin/skopeo --insecure-policy copy "nix:$spec" "$dest" "''${argv[@]}" ''; containerOptions = types.submodule ({ name, config, ... }: { options = { @@ -96,6 +121,16 @@ let default = "latest"; }; + rawBuildConfig = lib.mkOption { + type = types.attrsOf types.anything; + description = '' + Raw argument overrides to be passed down to nix2container.buildImage. + + see https://github.com/nlewo/nix2container#nix2containerbuildimage + ''; + default = { }; + }; + copyToRoot = lib.mkOption { type = types.nullOr types.path; description = "Add a path to the container. Defaults to the whole git repo."; @@ -104,9 +139,15 @@ let }; startupCommand = lib.mkOption { - type = types.nullOr (types.either types.str types.package); + type = types.nullOr (types.oneOf [ types.str types.package (types.listOf types.anything) ]); description = "Command to run in the container."; default = null; + apply = input: + let type = builtins.typeOf input; in + if type == "null" then [ ] + else if type == "string" then [ input ] + else if type == "list" then builtins.map builtins.toString input + else [ (builtins.toString input) ]; }; entrypoint = lib.mkOption { @@ -155,8 +196,44 @@ let internal = true; default = pkgs.writeScript "docker-run" '' #!${pkgs.bash}/bin/bash + set -eEuo pipefail + + container_args=() + runtime_args=() + + for arg in "$@" ; do + if [ "$arg" == "--" ] ; then + runtime_args=("''${container_args[@]}") + container_args=() + else + container_args+=("$arg") + fi + done + + docker run -it "''${runtime_args[@]}" '${config.name}:${config.version}' "''${container_args[@]}" + ''; + }; + + podmanRun = lib.mkOption { + type = types.package; + internal = true; + default = pkgs.writeScript "podman-run" '' + #!${pkgs.bash}/bin/bash + set -eEuo pipefail + + container_args=() + runtime_args=() + + for arg in "$@" ; do + if [ "$arg" == "--" ] ; then + runtime_args=("''${container_args[@]}") + container_args=() + else + container_args+=("$arg") + fi + done - docker run -it ${config.name}:${config.version} "$@" + podman run -it "''${runtime_args[@]}" '${config.name}:${config.version}' "''${container_args[@]}" ''; }; }; diff --git a/templates/flake-parts/.envrc b/templates/flake-parts/.envrc new file mode 100644 index 000000000..64038852a --- /dev/null +++ b/templates/flake-parts/.envrc @@ -0,0 +1,8 @@ +if ! has nix_direnv_version || ! nix_direnv_version 2.2.1; then + source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.2.1/direnvrc" "sha256-zelF0vLbEl5uaqrfIzbgNzJWGmLzCmYAkInj/LNxvKs=" +fi + +if ! use flake . --impure +then + echo "devenv could not be build. The devenv environment was not loaded. Make the necessary changes to devenv.nix and hit enter to try again." >&2 +fi diff --git a/templates/simple/.envrc b/templates/simple/.envrc index ce4386fd0..64038852a 100644 --- a/templates/simple/.envrc +++ b/templates/simple/.envrc @@ -2,9 +2,6 @@ if ! has nix_direnv_version || ! nix_direnv_version 2.2.1; then source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.2.1/direnvrc" "sha256-zelF0vLbEl5uaqrfIzbgNzJWGmLzCmYAkInj/LNxvKs=" fi -nix_direnv_watch_file devenv.nix -nix_direnv_watch_file devenv.lock -nix_direnv_watch_file devenv.yaml if ! use flake . --impure then echo "devenv could not be build. The devenv environment was not loaded. Make the necessary changes to devenv.nix and hit enter to try again." >&2