...
 
Commits (2)
{ nixpkgsPath ? import ./nixpkgs.nix }:
let
lib = import <nixpkgs/lib>;
lib = import "${toString nixpkgsPath}/lib";
nixosFunction = import "${toString nixpkgsPath}/nixos";
# the base config
baseConfig = { ... }: {
......@@ -40,7 +42,7 @@ let
};
# eval a config
doEval = config: import <nixpkgs/nixos> {
doEval = config: nixosFunction {
configuration = config;
};
......
......@@ -10,18 +10,24 @@ pkgs:
let
call = path: import path { inherit pkgs helpers; };
# some of the helpers where upstreamed to nixperiments
nixperiments = import (pkgs.fetchFromGitHub {
owner = "Profpatsch";
repo = "nixperiments";
rev = "b0de707f1e32adf9b29ff16bcaa30b6e08b65fea";
sha256 = "05498wn2r6j92li5kjv937y54v1yvvm6j0jbqzngzrq74mwy8ay2";
}) { nixpkgs = pkgs.path; };
in {
# -- GENERAL --
# generate an optparser for a bash script
script = {
inherit (call ./write-script-argparse)
withOptions optionChecks tests;
};
script = nixperiments.script;
# transform json files with nix code (jq but with nix)
inherit (call ./nix-json-trans.nix)
json2json json2string;
inherit (nixperiments)
json2json
json2string;
# prepend a folder to all files in a derivation output
prependPrefix = call ./prepend-prefix.nix;
......
{ pkgs, helpers, libPath ? <nixpkgs/lib> }:
# Scripts to transform json via nix eval.
# See the tests below for examples.
# TODO: Do we need all of lib?
let
# Produce nix files that read the input files
# and then call `nix eval`.
transGeneric = name: synopsis: nixCommand: with helpers.script;
withOptions {
inherit name synopsis;
description = ''
Similar to jq, but arguably more powerful.
The nix script `trans` is a function that takes
`args` as first argument (a nix attrset)
and a `json` file as a nix value as second argument.
`trans` has the <nixpkgs/lib> attrset in scope.
Through `args` it is possible to pass e.g.
nix store paths or files or more complex data.
'';
options = {
args = {
description = "Nix arguments to pass to the transformer.";
checks = [ optionChecks.fileExists ];
};
trans = {
description = "Nix JSON transformer function.";
checks = [ optionChecks.fileExists ];
};
json = {
description = "JSON input data";
checks = [ optionChecks.fileExists ];
};
};
script = ''
#!${pkgs.stdenv.shell}
# nix needs a path, containing / (`args` could be e.g. 'myfolder').
ARGS="$(realpath "$args")"
DIR=$(mktemp -d)
# TODO: script-input should contain the name of `trans`
# to ease debugging
cat >$DIR/script-input.nix <<EOF
with import ${libPath};
$(cat "$trans")
EOF
JSON="$(realpath "$json")"
cat >$DIR/script.nix <<EOF
let
args = import $ARGS;
f = import $DIR/script-input.nix;
json = builtins.fromJSON (builtins.readFile $JSON);
in
assert builtins.typeOf args == "set";
assert builtins.typeOf f == "lambda";
assert builtins.typeOf $JSON == "path";
f args json
EOF
source ${pkgs.setupLocalNixStore}
${nixCommand}
'';
};
# `nix eval` call for json output
json2json = transGeneric
"nix-json-to-json"
"Transform a json file using a nix expression."
''
${pkgs.nix}/bin/nix eval \
--json \
--show-trace \
-f $DIR/script.nix \
""
'';
# `nix eval` call for plain text output
json2string = transGeneric
"nix-json-to-out"
"Convert a json file to a string using a nix expression."
''
${pkgs.nix}/bin/nix eval \
--raw \
--show-trace \
-f $DIR/script.nix \
""
'';
mkjson = json: pkgs.writeText "test.json" (builtins.toJSON json);
jsonTests =
let
# compare json2json output with given result file
eq = nixFile: jsonFile: resultFile: ''
echo "{}" > ./args.nix
${pkgs.diffutils}/bin/diff \
<(${json2json} \
--args ./args.nix \
--trans ${nixFile} \
--json ${jsonFile}) \
${resultFile}
'';
in {
# the identity function should produce the same output
# we can compare bit-by-bit because the input is produced
# by nix as well in this case
idJson =
let
json = mkjson {
foo = [ 1 2 3 null "bar" ];
};
idScr = pkgs.writeText "id-converter" "{}: id";
in eq idScr json json;
# we can apply library functions, like `mapAttrs`
replaceAttrsJson =
eq (pkgs.writeText "maybe-its-neuer"
''{}: mapAttrs (_: _: "manuel neuer")'')
(mkjson { foo = "richard stallman"; bar = "linus torvalds"; })
(mkjson { foo = "manuel neuer"; bar = "manuel neuer"; });
};
outTests = {
# the string is echoed as-is
buildEchoScriptFromJsonString = ''
echo "{}" > ./args.nix
echo "{}: str: str" > ./echo.nix
${json2string} \
--args=./args.nix \
--trans=./echo.nix \
--json=${mkjson "hello!\nworld!"} \
> out
grep "hello!" <out
grep "world!" <out
'';
# this one is interesting, we don’t transform at all
# but rather use the fact that additional stuff can
# be passed by args, e.g. store paths to executables
passingArguments =
let
args = pkgs.writeText "args.nix" ''
{ shell = "${pkgs.stdenv.shell}"; }
'';
echoshell = pkgs.writeText "echoshell.nix" ''
{ shell }: _: '''
#!''${shell}
echo echoshell
'''
'';
in ''
touch empty.json
# we produce a script in the `trans` expression,
# print it, eval it and check its output
${json2string} \
--args ${args} \
--trans ${echoshell} \
--json empty.json \
| ${pkgs.stdenv.shell} -s \
| grep echoshell
'';
};
in {
json2json = pkgs.withTests jsonTests json2json;
json2string = pkgs.withTests outTests json2string;
}
{ stdenv, lib, writeText, writeScript, runCommand, utillinux }:
# Create a bash script and automatically generate an option parser.
# The option names are put into scope as plain bash variables.
{
# script name
# : string
name,
# short synopsis (shown in usage)
# : string
synopsis,
# long (multi-line) description
# : lines
description ? "",
# attrset of option names;
# the key is used as name,
# the value if of type
# `{ description : string , checks : check }`
# where `check` is of type
# { fnName : string # check bash function name
# , name : string # name displayed in usage
# , code : lines # bash code of the check
# }
options,
# bash script that has all options in scope
# as variables of the same name
# : lines
script
}:
let
# the usage text
usage =
let
checks = lib.concatMapStringsSep ", " (c: c.name);
usageAttr = n: v: "--${n} (${checks v.checks}): ${v.description}";
in
writeText "${name}-usage.txt" ''
${name}: ${synopsis}
${description}
${name}
${builtins.concatStringsSep "\n "
(lib.mapAttrsToList usageAttr options)}
'';
# bash function that prints usage to stderr
usageFn = ''
function USAGE__ {
cat 1>&2 <<EOF
ERROR: $(echo -e $1)
$(cat ${usage})
EOF
exit 1
}
'';
# all checks we are using in this script
ourChecks =
# we can remove duplicate checks because they are static
lib.unique
(builtins.concatLists
(lib.mapAttrsToList (_: opt: opt.checks) options));
# check bash functions
checkFns =
let
checkFn = c: ''
function ${c.fnName} {
${c.code}
} '';
in lib.concatMapStringsSep "\n" checkFn ourChecks;
nameMapOptsSep = sep: f: lib.concatMapStringsSep sep f
(builtins.attrNames options);
# bash getopt invocation
getopt =
let
opts = nameMapOptsSep "," (o: "${o}:");
getoptBin = runCommand "getopt-bin" {} ''
install -D ${lib.getBin utillinux}/bin/getopt \
$out/bin/getopt
'';
in ''
PARSED__=$(${getoptBin}/bin/getopt --name="${name}" \
--options= \
--longoptions=${opts} \
-- "$@")
# read getopt’s output this way to handle the quoting right:
eval set -- "$PARSED__"
'';
# parsing the getopt output
parseArguments =
let
# this is probably not very efficient …
# a small embedding for indentation inside
# lists of lists of strings
rep = n: ch: builtins.foldl' (str: _: str + ch) ""
(builtins.genList lib.id n);
indent = n: list: (map (s: (rep n " ") + s) list);
i4 = indent 4;
i2 = indent 2;
# pure
i0 = s: [s];
# join
embed = builtins.concatLists;
applyIndent = builtins.concatStringsSep "\n";
# a check inside an argument handler
runCheck = argName: c: embed [
(i0 ''${c.fnName} "$2" \'')
(i2 (i0 ''|| ERRS__+="--${argName}: file '$2' does not exist\n"''))
];
# generated argument handler for each option
argHandler = name: opt: embed [
(i0 ''--${name})'')
(i2 (embed (map (runCheck name) opt.checks)))
(i2 [
''${name}="$2"''
''shift 2''
'';;''
])
];
in ''
# accumulate errors
ERRS_=
# parse getopt output
while true; do
case "$1" in
${applyIndent
(i4 (embed (lib.mapAttrsToList argHandler options)))}
--)
shift
# no further arguments
[[ $# -ne 0 ]] \
&& ERRS__+="too many arguments: $@"
break
;;
*)
ERRS__+="unknown argument: $1\n"
shift 1
;;
esac
done
# check if there were errors
[[ "$ERRS__" != "" ]] \
&& USAGE__ "Argument errors:\n$ERRS__"
'';
# we abort on missing options
checkAllOptionsGiven = ''
# check whether all options have been given
ERRS__=
for opt in ${nameMapOptsSep " " lib.id}; do
test -v $opt \
|| ERRS__+=" --$opt"
done
[[ "$ERRS__" != "" ]] \
&& USAGE__ "options$ERRS__ are required"
'';
# the optparser, which is sourced by the final script
optParser = writeText "${name}-optparser.sh" ''
# This is an automatically generated optparser.
# It sets the following bash variables:
# ${nameMapOptsSep ", " lib.id}
# Inspired by:
# https://stackoverflow.com/a/29754866/1382925
${usageFn}
[[ $# -eq 0 ]] && USAGE__ "no arguments given"
${checkFns}
${getopt}
${parseArguments}
${checkAllOptionsGiven}
# unset all variables, as to not lead to strange
# effects in the following script
unset -v PARSED__ ERRS__
unset -f USAGE__
unset -f ${lib.concatMapStringsSep " " (c: c.fnName) ourChecks}
'';
# the optparser is sourced because the generated code is quite long
# and the actual script logic should not be shadowed by that.
# TODO: maybe invert it, that you call the argparser yourself?
finalScript = writeScript name ''
#!${stdenv.shell}
# call the argparser, which sets the following variables:
# ${nameMapOptsSep ", " lib.id}
source ${optParser}
${script}
'';
in
finalScript
{ pkgs, helpers }:
let
# main documentation in here
withOptions = pkgs.callPackage ./build-script.nix {};
# A list of checks that can be passed to `script.withOptions`.
optionChecks = {
fileExists = {
fnName = "FILE_EXISTS__";
name = "FILE";
code = ''test -a "$1"'';
};
};
# TODO: nice tests
tests = {
foo = withOptions {
name = "myname";
synopsis = "dis is synopsis";
options = {
args = {
description = "argument description";
checks = [ optionChecks.fileExists ];
};
json = {
description = "some json!";
checks = [];
};
};
script = ''
echo $args
echo $json
'';
};
};
in {
inherit withOptions optionChecks tests;
}
#!/usr/bin/env bash
# Inspired by:
# https://stackoverflow.com/a/29754866/1382925
function USAGE__ {
# TODO: EOF must not appear in interpolation
cat 1>&2 <<EOF
ERROR: $(echo -e $1)
myname: dis is description
myname
--args (TYPE): argument description
EOF
exit 1
}
function CHECK_FILE__ {
test -a "$1"
}
[[ $# -eq 0 ]] && USAGE__ "no arguments given"
PARSED__=$(getopt --name=myname --options= --longoptions=args:,script:,json: -- "$@")
# read getopt’s output this way to handle the quoting right:
eval set -- "$PARSED__"
# parse arguments, run checks, accumulate errors
ERRS__=
while true; do
case "$1" in
--args)
CHECK_FILE__ "$2" \
|| ERRS__+="--args: file '$2' does not exist\n"
args="$2"
shift 2
;;
--)
shift
# no further arguments
[[ $# -ne 0 ]] \
&& ERRS__+="too many arguments: $@"
break
;;
*)
ERRS__+="unknown argument: $1\n"
shift 1
;;
esac
done
[[ "$ERRS__" != "" ]] \
&& USAGE__ "Argument errors:\n$ERRS__"
# check whether all options have been given
ERRS__=
for opt in args script json; do
test -v args \
|| ERRS__+=" --${opt}"
done
[[ "$ERRS__" != "" ]] \
&& USAGE__ "options$ERRS__ are required"
# TODO: make sure these are not used as arguments
unset -v PARSED__ ERRS__
unset -f USAGE__
unset -f CHECK_FILE__
\ No newline at end of file
# a version of nixpkgs that has a few additional patches
# to enable the nix definitions in this folder.
# Plus, it’s pinned to a well-known working state.
let
domain = "gitlab.techcultivation.org";
owner = "Profpatsch";
repo = "nixpkgs-techcultivation";
rev = "a04fa4343aaab8ca0b5f6382522b32d18dc60504";
sha256 = "0s3wf4wb2rg4j7f326bi1iaxgilh5i1qp0hgq58mmyv2sp14jcmv";
in builtins.fetchTarball {
# taken from the nixpkgs definition of `fetchFromGitLab`
url = "https://${domain}/api/v4/projects/${owner}%2F${repo}/repository/archive.tar.gz?sha=${rev}";
inherit sha256;
}