Skip to content

Commit 2d5b1b2

Browse files
committed
Pass lists/attrsets to bash as (associative) arrays
1 parent ac12517 commit 2d5b1b2

File tree

10 files changed

+166
-26
lines changed

10 files changed

+166
-26
lines changed

src/libexpr/primops.cc

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -713,7 +713,7 @@ static void prim_derivationStrict(EvalState & state, const Pos & pos, Value * *
713713
if (outputHashRecursive) outputHashAlgo = "r:" + outputHashAlgo;
714714

715715
Path outPath = state.store->makeFixedOutputPath(outputHashRecursive, h, drvName);
716-
drv.env["out"] = outPath;
716+
if (!jsonObject) drv.env["out"] = outPath;
717717
drv.outputs["out"] = DerivationOutput(outPath, outputHashAlgo, *outputHash);
718718
}
719719

@@ -724,7 +724,7 @@ static void prim_derivationStrict(EvalState & state, const Pos & pos, Value * *
724724
an empty value. This ensures that changes in the set of
725725
output names do get reflected in the hash. */
726726
for (auto & i : outputs) {
727-
drv.env[i] = "";
727+
if (!jsonObject) drv.env[i] = "";
728728
drv.outputs[i] = DerivationOutput("", "", "");
729729
}
730730

@@ -735,7 +735,7 @@ static void prim_derivationStrict(EvalState & state, const Pos & pos, Value * *
735735
for (auto & i : drv.outputs)
736736
if (i.second.path == "") {
737737
Path outPath = state.store->makeOutputPath(i.first, h, drvName);
738-
drv.env[i.first] = outPath;
738+
if (!jsonObject) drv.env[i.first] = outPath;
739739
i.second.path = outPath;
740740
}
741741
}

src/libstore/build.cc

Lines changed: 93 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include <thread>
1919
#include <future>
2020
#include <chrono>
21+
#include <regex>
2122

2223
#include <limits.h>
2324
#include <sys/time.h>
@@ -55,6 +56,8 @@
5556
#include <sys/statvfs.h>
5657
#endif
5758

59+
#include <nlohmann/json.hpp>
60+
5861

5962
namespace nix {
6063

@@ -2286,12 +2289,99 @@ void DerivationGoal::initEnv()
22862289
}
22872290

22882291

2292+
static std::regex shVarName("[A-Za-z_][A-Za-z0-9_]*");
2293+
2294+
22892295
void DerivationGoal::writeStructuredAttrs()
22902296
{
2291-
auto json = drv->env.find("__json");
2292-
if (json == drv->env.end()) return;
2297+
auto jsonAttr = drv->env.find("__json");
2298+
if (jsonAttr == drv->env.end()) return;
2299+
2300+
try {
2301+
2302+
auto jsonStr = rewriteStrings(jsonAttr->second, inputRewrites);
2303+
2304+
auto json = nlohmann::json::parse(jsonStr);
2305+
2306+
/* Add an "outputs" object containing the output paths. */
2307+
nlohmann::json outputs;
2308+
for (auto & i : drv->outputs)
2309+
outputs[i.first] = rewriteStrings(i.second.path, inputRewrites);
2310+
json["outputs"] = outputs;
2311+
2312+
writeFile(tmpDir + "/.attrs.json", json.dump());
2313+
2314+
/* As a convenience to bash scripts, write a shell file that
2315+
maps all attributes that are representable in bash -
2316+
namely, strings, integers, nulls, Booleans, and arrays and
2317+
objects consisting entirely of those values. (So nested
2318+
arrays or objects are not supported.) */
2319+
2320+
auto handleSimpleType = [](const nlohmann::json & value) -> std::experimental::optional<std::string> {
2321+
if (value.is_string())
2322+
return shellEscape(value);
2323+
2324+
if (value.is_number()) {
2325+
auto f = value.get<float>();
2326+
if (std::ceil(f) == f)
2327+
return std::to_string(value.get<int>());
2328+
}
2329+
2330+
if (value.is_null())
2331+
return "''";
2332+
2333+
if (value.is_boolean())
2334+
return value.get<bool>() ? "1" : "";
2335+
2336+
return {};
2337+
};
2338+
2339+
std::string jsonSh;
22932340

2294-
writeFile(tmpDir + "/.attrs.json", rewriteStrings(json->second, inputRewrites));
2341+
for (auto i = json.begin(); i != json.end(); ++i) {
2342+
2343+
if (!std::regex_match(i.key(), shVarName)) continue;
2344+
2345+
auto & value = i.value();
2346+
2347+
auto s = handleSimpleType(value);
2348+
if (s)
2349+
jsonSh += fmt("declare %s=%s\n", i.key(), *s);
2350+
2351+
else if (value.is_array()) {
2352+
std::string s2;
2353+
bool good = true;
2354+
2355+
for (auto i = value.begin(); i != value.end(); ++i) {
2356+
auto s3 = handleSimpleType(i.value());
2357+
if (!s3) { good = false; break; }
2358+
s2 += *s3; s2 += ' ';
2359+
}
2360+
2361+
if (good)
2362+
jsonSh += fmt("declare -a %s=(%s)\n", i.key(), s2);
2363+
}
2364+
2365+
else if (value.is_object()) {
2366+
std::string s2;
2367+
bool good = true;
2368+
2369+
for (auto i = value.begin(); i != value.end(); ++i) {
2370+
auto s3 = handleSimpleType(i.value());
2371+
if (!s3) { good = false; break; }
2372+
s2 += fmt("[%s]=%s ", shellEscape(i.key()), *s3);
2373+
}
2374+
2375+
if (good)
2376+
jsonSh += fmt("declare -A %s=(%s)\n", i.key(), s2);
2377+
}
2378+
}
2379+
2380+
writeFile(tmpDir + "/.attrs.sh", jsonSh);
2381+
2382+
} catch (std::exception & e) {
2383+
throw Error("cannot process __json attribute of '%s': %s", drvPath, e.what());
2384+
}
22952385
}
22962386

22972387

src/libutil/util.cc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1142,6 +1142,16 @@ std::string toLower(const std::string & s)
11421142
}
11431143

11441144

1145+
std::string shellEscape(const std::string & s)
1146+
{
1147+
std::string r = "'";
1148+
for (auto & i : s)
1149+
if (i == '\'') r += "'\\''"; else r += i;
1150+
r += '\'';
1151+
return r;
1152+
}
1153+
1154+
11451155
void ignoreException()
11461156
{
11471157
try {

src/libutil/util.hh

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -352,10 +352,8 @@ bool hasSuffix(const string & s, const string & suffix);
352352
std::string toLower(const std::string & s);
353353

354354

355-
/* Escape a string that contains octal-encoded escape codes such as
356-
used in /etc/fstab and /proc/mounts (e.g. "foo\040bar" decodes to
357-
"foo bar"). */
358-
string decodeOctalEscaped(const string & s);
355+
/* Escape a string as a shell word. */
356+
std::string shellEscape(const std::string & s);
359357

360358

361359
/* Exception handling in destructors: print an error message, then

src/nix-build/nix-build.cc

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -196,10 +196,6 @@ void mainWrapped(int argc, char * * argv)
196196
interactive = false;
197197
auto execArgs = "";
198198

199-
auto shellEscape = [](const string & s) {
200-
return "'" + std::regex_replace(s, std::regex("'"), "'\\''") + "'";
201-
};
202-
203199
// Überhack to support Perl. Perl examines the shebang and
204200
// executes it unless it contains the string "perl" or "indir",
205201
// or (undocumented) argv[0] does not contain "perl". Exploit

src/nix-store/nix-store.cc

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -440,15 +440,6 @@ static void opQuery(Strings opFlags, Strings opArgs)
440440
}
441441

442442

443-
static string shellEscape(const string & s)
444-
{
445-
string r;
446-
for (auto & i : s)
447-
if (i == '\'') r += "'\\''"; else r += i;
448-
return r;
449-
}
450-
451-
452443
static void opPrintEnv(Strings opFlags, Strings opArgs)
453444
{
454445
if (!opFlags.empty()) throw UsageError("unknown flag");
@@ -460,7 +451,7 @@ static void opPrintEnv(Strings opFlags, Strings opArgs)
460451
/* Print each environment variable in the derivation in a format
461452
that can be sourced by the shell. */
462453
for (auto & i : drv.env)
463-
cout << format("export %1%; %1%='%2%'\n") % i.first % shellEscape(i.second);
454+
cout << format("export %1%; %1%=%2%\n") % i.first % shellEscape(i.second);
464455

465456
/* Also output the arguments. This doesn't preserve whitespace in
466457
arguments. */

tests/config.nix

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ rec {
1313
derivation ({
1414
inherit system;
1515
builder = shell;
16-
args = ["-e" args.builder or (builtins.toFile "builder.sh" "eval \"$buildCommand\"")];
16+
args = ["-e" args.builder or (builtins.toFile "builder.sh" "if [ -e .attrs.sh ]; then source .attrs.sh; fi; eval \"$buildCommand\"")];
1717
PATH = path;
1818
} // removeAttrs args ["builder" "meta"])
1919
// { meta = args.meta or {}; };

tests/local.mk

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ nix_tests = \
1414
placeholders.sh nix-shell.sh \
1515
linux-sandbox.sh \
1616
build-remote.sh \
17-
nar-index.sh
17+
nar-index.sh \
18+
structured-attrs.sh
1819
# parallel.sh
1920

2021
install-tests += $(foreach x, $(nix_tests), tests/$(x))

tests/structured-attrs.nix

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
with import ./config.nix;
2+
3+
mkDerivation {
4+
name = "structured";
5+
6+
__structuredAttrs = true;
7+
8+
buildCommand = ''
9+
set -x
10+
11+
[[ $int = 123456789 ]]
12+
[[ -z $float ]]
13+
[[ -n $boolTrue ]]
14+
[[ -z $boolFalse ]]
15+
[[ -n ''${hardening[format]} ]]
16+
[[ -z ''${hardening[fortify]} ]]
17+
[[ ''${#buildInputs[@]} = 7 ]]
18+
[[ ''${buildInputs[2]} = c ]]
19+
[[ -v nothing ]]
20+
[[ -z $nothing ]]
21+
22+
mkdir ''${outputs[out]}
23+
echo bar > $dest
24+
'';
25+
26+
buildInputs = [ "a" "b" "c" 123 "'" "\"" null ];
27+
28+
hardening.format = true;
29+
hardening.fortify = false;
30+
31+
outer.inner = [ 1 2 3 ];
32+
33+
int = 123456789;
34+
35+
float = 123.456;
36+
37+
boolTrue = true;
38+
boolFalse = false;
39+
40+
nothing = null;
41+
42+
dest = "${placeholder "out"}/foo";
43+
44+
"foo bar" = "BAD";
45+
"1foobar" = "BAD";
46+
"foo$" = "BAD";
47+
}

tests/structured-attrs.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
source common.sh
2+
3+
clearStore
4+
5+
outPath=$(nix-build structured-attrs.nix --no-out-link)
6+
7+
[[ $(cat $outPath/foo) = bar ]]

0 commit comments

Comments
 (0)