Pass lists/attrsets to bash as (associative) arrays
This commit is contained in:
		
							parent
							
								
									ac12517f3e
								
							
						
					
					
						commit
						2d5b1b24bf
					
				
					 10 changed files with 166 additions and 26 deletions
				
			
		|  | @ -713,7 +713,7 @@ static void prim_derivationStrict(EvalState & state, const Pos & pos, Value * * | ||||||
|         if (outputHashRecursive) outputHashAlgo = "r:" + outputHashAlgo; |         if (outputHashRecursive) outputHashAlgo = "r:" + outputHashAlgo; | ||||||
| 
 | 
 | ||||||
|         Path outPath = state.store->makeFixedOutputPath(outputHashRecursive, h, drvName); |         Path outPath = state.store->makeFixedOutputPath(outputHashRecursive, h, drvName); | ||||||
|         drv.env["out"] = outPath; |         if (!jsonObject) drv.env["out"] = outPath; | ||||||
|         drv.outputs["out"] = DerivationOutput(outPath, outputHashAlgo, *outputHash); |         drv.outputs["out"] = DerivationOutput(outPath, outputHashAlgo, *outputHash); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -724,7 +724,7 @@ static void prim_derivationStrict(EvalState & state, const Pos & pos, Value * * | ||||||
|            an empty value.  This ensures that changes in the set of |            an empty value.  This ensures that changes in the set of | ||||||
|            output names do get reflected in the hash. */ |            output names do get reflected in the hash. */ | ||||||
|         for (auto & i : outputs) { |         for (auto & i : outputs) { | ||||||
|             drv.env[i] = ""; |             if (!jsonObject) drv.env[i] = ""; | ||||||
|             drv.outputs[i] = DerivationOutput("", "", ""); |             drv.outputs[i] = DerivationOutput("", "", ""); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -735,7 +735,7 @@ static void prim_derivationStrict(EvalState & state, const Pos & pos, Value * * | ||||||
|         for (auto & i : drv.outputs) |         for (auto & i : drv.outputs) | ||||||
|             if (i.second.path == "") { |             if (i.second.path == "") { | ||||||
|                 Path outPath = state.store->makeOutputPath(i.first, h, drvName); |                 Path outPath = state.store->makeOutputPath(i.first, h, drvName); | ||||||
|                 drv.env[i.first] = outPath; |                 if (!jsonObject) drv.env[i.first] = outPath; | ||||||
|                 i.second.path = outPath; |                 i.second.path = outPath; | ||||||
|             } |             } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -18,6 +18,7 @@ | ||||||
| #include <thread> | #include <thread> | ||||||
| #include <future> | #include <future> | ||||||
| #include <chrono> | #include <chrono> | ||||||
|  | #include <regex> | ||||||
| 
 | 
 | ||||||
| #include <limits.h> | #include <limits.h> | ||||||
| #include <sys/time.h> | #include <sys/time.h> | ||||||
|  | @ -55,6 +56,8 @@ | ||||||
| #include <sys/statvfs.h> | #include <sys/statvfs.h> | ||||||
| #endif | #endif | ||||||
| 
 | 
 | ||||||
|  | #include <nlohmann/json.hpp> | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| namespace nix { | namespace nix { | ||||||
| 
 | 
 | ||||||
|  | @ -2286,12 +2289,99 @@ void DerivationGoal::initEnv() | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | static std::regex shVarName("[A-Za-z_][A-Za-z0-9_]*"); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| void DerivationGoal::writeStructuredAttrs() | void DerivationGoal::writeStructuredAttrs() | ||||||
| { | { | ||||||
|     auto json = drv->env.find("__json"); |     auto jsonAttr = drv->env.find("__json"); | ||||||
|     if (json == drv->env.end()) return; |     if (jsonAttr == drv->env.end()) return; | ||||||
| 
 | 
 | ||||||
|     writeFile(tmpDir + "/.attrs.json", rewriteStrings(json->second, inputRewrites)); |     try { | ||||||
|  | 
 | ||||||
|  |         auto jsonStr = rewriteStrings(jsonAttr->second, inputRewrites); | ||||||
|  | 
 | ||||||
|  |         auto json = nlohmann::json::parse(jsonStr); | ||||||
|  | 
 | ||||||
|  |         /* Add an "outputs" object containing the output paths. */ | ||||||
|  |         nlohmann::json outputs; | ||||||
|  |         for (auto & i : drv->outputs) | ||||||
|  |             outputs[i.first] = rewriteStrings(i.second.path, inputRewrites); | ||||||
|  |         json["outputs"] = outputs; | ||||||
|  | 
 | ||||||
|  |         writeFile(tmpDir + "/.attrs.json", json.dump()); | ||||||
|  | 
 | ||||||
|  |         /* As a convenience to bash scripts, write a shell file that
 | ||||||
|  |            maps all attributes that are representable in bash - | ||||||
|  |            namely, strings, integers, nulls, Booleans, and arrays and | ||||||
|  |            objects consisting entirely of those values. (So nested | ||||||
|  |            arrays or objects are not supported.) */ | ||||||
|  | 
 | ||||||
|  |         auto handleSimpleType = [](const nlohmann::json & value) -> std::experimental::optional<std::string> { | ||||||
|  |             if (value.is_string()) | ||||||
|  |                 return shellEscape(value); | ||||||
|  | 
 | ||||||
|  |             if (value.is_number()) { | ||||||
|  |                 auto f = value.get<float>(); | ||||||
|  |                 if (std::ceil(f) == f) | ||||||
|  |                     return std::to_string(value.get<int>()); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (value.is_null()) | ||||||
|  |                 return "''"; | ||||||
|  | 
 | ||||||
|  |             if (value.is_boolean()) | ||||||
|  |                 return value.get<bool>() ? "1" : ""; | ||||||
|  | 
 | ||||||
|  |             return {}; | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         std::string jsonSh; | ||||||
|  | 
 | ||||||
|  |         for (auto i = json.begin(); i != json.end(); ++i) { | ||||||
|  | 
 | ||||||
|  |             if (!std::regex_match(i.key(), shVarName)) continue; | ||||||
|  | 
 | ||||||
|  |             auto & value = i.value(); | ||||||
|  | 
 | ||||||
|  |             auto s = handleSimpleType(value); | ||||||
|  |             if (s) | ||||||
|  |                 jsonSh += fmt("declare %s=%s\n", i.key(), *s); | ||||||
|  | 
 | ||||||
|  |             else if (value.is_array()) { | ||||||
|  |                 std::string s2; | ||||||
|  |                 bool good = true; | ||||||
|  | 
 | ||||||
|  |                 for (auto i = value.begin(); i != value.end(); ++i) { | ||||||
|  |                     auto s3 = handleSimpleType(i.value()); | ||||||
|  |                     if (!s3) { good = false; break; } | ||||||
|  |                     s2 += *s3; s2 += ' '; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 if (good) | ||||||
|  |                     jsonSh += fmt("declare -a %s=(%s)\n", i.key(), s2); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             else if (value.is_object()) { | ||||||
|  |                 std::string s2; | ||||||
|  |                 bool good = true; | ||||||
|  | 
 | ||||||
|  |                 for (auto i = value.begin(); i != value.end(); ++i) { | ||||||
|  |                     auto s3 = handleSimpleType(i.value()); | ||||||
|  |                     if (!s3) { good = false; break; } | ||||||
|  |                     s2 += fmt("[%s]=%s ", shellEscape(i.key()), *s3); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 if (good) | ||||||
|  |                     jsonSh += fmt("declare -A %s=(%s)\n", i.key(), s2); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         writeFile(tmpDir + "/.attrs.sh", jsonSh); | ||||||
|  | 
 | ||||||
|  |     } catch (std::exception & e) { | ||||||
|  |         throw Error("cannot process __json attribute of '%s': %s", drvPath, e.what()); | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1142,6 +1142,16 @@ std::string toLower(const std::string & s) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | std::string shellEscape(const std::string & s) | ||||||
|  | { | ||||||
|  |     std::string r = "'"; | ||||||
|  |     for (auto & i : s) | ||||||
|  |         if (i == '\'') r += "'\\''"; else r += i; | ||||||
|  |     r += '\''; | ||||||
|  |     return r; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| void ignoreException() | void ignoreException() | ||||||
| { | { | ||||||
|     try { |     try { | ||||||
|  |  | ||||||
|  | @ -352,10 +352,8 @@ bool hasSuffix(const string & s, const string & suffix); | ||||||
| std::string toLower(const std::string & s); | std::string toLower(const std::string & s); | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| /* Escape a string that contains octal-encoded escape codes such as
 | /* Escape a string as a shell word. */ | ||||||
|    used in /etc/fstab and /proc/mounts (e.g. "foo\040bar" decodes to | std::string shellEscape(const std::string & s); | ||||||
|    "foo bar"). */ |  | ||||||
| string decodeOctalEscaped(const string & s); |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| /* Exception handling in destructors: print an error message, then
 | /* Exception handling in destructors: print an error message, then
 | ||||||
|  |  | ||||||
|  | @ -196,10 +196,6 @@ void mainWrapped(int argc, char * * argv) | ||||||
|             interactive = false; |             interactive = false; | ||||||
|             auto execArgs = ""; |             auto execArgs = ""; | ||||||
| 
 | 
 | ||||||
|             auto shellEscape = [](const string & s) { |  | ||||||
|                 return "'" + std::regex_replace(s, std::regex("'"), "'\\''") + "'"; |  | ||||||
|             }; |  | ||||||
| 
 |  | ||||||
|             // Überhack to support Perl. Perl examines the shebang and
 |             // Überhack to support Perl. Perl examines the shebang and
 | ||||||
|             // executes it unless it contains the string "perl" or "indir",
 |             // executes it unless it contains the string "perl" or "indir",
 | ||||||
|             // or (undocumented) argv[0] does not contain "perl". Exploit
 |             // or (undocumented) argv[0] does not contain "perl". Exploit
 | ||||||
|  |  | ||||||
|  | @ -440,15 +440,6 @@ static void opQuery(Strings opFlags, Strings opArgs) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| static string shellEscape(const string & s) |  | ||||||
| { |  | ||||||
|     string r; |  | ||||||
|     for (auto & i : s) |  | ||||||
|         if (i == '\'') r += "'\\''"; else r += i; |  | ||||||
|     return r; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| static void opPrintEnv(Strings opFlags, Strings opArgs) | static void opPrintEnv(Strings opFlags, Strings opArgs) | ||||||
| { | { | ||||||
|     if (!opFlags.empty()) throw UsageError("unknown flag"); |     if (!opFlags.empty()) throw UsageError("unknown flag"); | ||||||
|  | @ -460,7 +451,7 @@ static void opPrintEnv(Strings opFlags, Strings opArgs) | ||||||
|     /* Print each environment variable in the derivation in a format
 |     /* Print each environment variable in the derivation in a format
 | ||||||
|        that can be sourced by the shell. */ |        that can be sourced by the shell. */ | ||||||
|     for (auto & i : drv.env) |     for (auto & i : drv.env) | ||||||
|         cout << format("export %1%; %1%='%2%'\n") % i.first % shellEscape(i.second); |         cout << format("export %1%; %1%=%2%\n") % i.first % shellEscape(i.second); | ||||||
| 
 | 
 | ||||||
|     /* Also output the arguments.  This doesn't preserve whitespace in
 |     /* Also output the arguments.  This doesn't preserve whitespace in
 | ||||||
|        arguments. */ |        arguments. */ | ||||||
|  |  | ||||||
|  | @ -13,7 +13,7 @@ rec { | ||||||
|     derivation ({ |     derivation ({ | ||||||
|       inherit system; |       inherit system; | ||||||
|       builder = shell; |       builder = shell; | ||||||
|       args = ["-e" args.builder or (builtins.toFile "builder.sh" "eval \"$buildCommand\"")]; |       args = ["-e" args.builder or (builtins.toFile "builder.sh" "if [ -e .attrs.sh ]; then source .attrs.sh; fi; eval \"$buildCommand\"")]; | ||||||
|       PATH = path; |       PATH = path; | ||||||
|     } // removeAttrs args ["builder" "meta"]) |     } // removeAttrs args ["builder" "meta"]) | ||||||
|     // { meta = args.meta or {}; }; |     // { meta = args.meta or {}; }; | ||||||
|  |  | ||||||
|  | @ -14,7 +14,8 @@ nix_tests = \ | ||||||
|   placeholders.sh nix-shell.sh \
 |   placeholders.sh nix-shell.sh \
 | ||||||
|   linux-sandbox.sh \
 |   linux-sandbox.sh \
 | ||||||
|   build-remote.sh \
 |   build-remote.sh \
 | ||||||
|   nar-index.sh |   nar-index.sh \
 | ||||||
|  |   structured-attrs.sh | ||||||
|   # parallel.sh |   # parallel.sh | ||||||
| 
 | 
 | ||||||
| install-tests += $(foreach x, $(nix_tests), tests/$(x)) | install-tests += $(foreach x, $(nix_tests), tests/$(x)) | ||||||
|  |  | ||||||
							
								
								
									
										47
									
								
								tests/structured-attrs.nix
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								tests/structured-attrs.nix
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,47 @@ | ||||||
|  | with import ./config.nix; | ||||||
|  | 
 | ||||||
|  | mkDerivation { | ||||||
|  |   name = "structured"; | ||||||
|  | 
 | ||||||
|  |   __structuredAttrs = true; | ||||||
|  | 
 | ||||||
|  |   buildCommand = '' | ||||||
|  |     set -x | ||||||
|  | 
 | ||||||
|  |     [[ $int = 123456789 ]] | ||||||
|  |     [[ -z $float ]] | ||||||
|  |     [[ -n $boolTrue ]] | ||||||
|  |     [[ -z $boolFalse ]] | ||||||
|  |     [[ -n ''${hardening[format]} ]] | ||||||
|  |     [[ -z ''${hardening[fortify]} ]] | ||||||
|  |     [[ ''${#buildInputs[@]} = 7 ]] | ||||||
|  |     [[ ''${buildInputs[2]} = c ]] | ||||||
|  |     [[ -v nothing ]] | ||||||
|  |     [[ -z $nothing ]] | ||||||
|  | 
 | ||||||
|  |     mkdir ''${outputs[out]} | ||||||
|  |     echo bar > $dest | ||||||
|  |   ''; | ||||||
|  | 
 | ||||||
|  |   buildInputs = [ "a" "b" "c" 123 "'" "\"" null ]; | ||||||
|  | 
 | ||||||
|  |   hardening.format = true; | ||||||
|  |   hardening.fortify = false; | ||||||
|  | 
 | ||||||
|  |   outer.inner = [ 1 2 3 ]; | ||||||
|  | 
 | ||||||
|  |   int = 123456789; | ||||||
|  | 
 | ||||||
|  |   float = 123.456; | ||||||
|  | 
 | ||||||
|  |   boolTrue = true; | ||||||
|  |   boolFalse = false; | ||||||
|  | 
 | ||||||
|  |   nothing = null; | ||||||
|  | 
 | ||||||
|  |   dest = "${placeholder "out"}/foo"; | ||||||
|  | 
 | ||||||
|  |   "foo bar" = "BAD"; | ||||||
|  |   "1foobar" = "BAD"; | ||||||
|  |   "foo$" = "BAD"; | ||||||
|  | } | ||||||
							
								
								
									
										7
									
								
								tests/structured-attrs.sh
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								tests/structured-attrs.sh
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | ||||||
|  | source common.sh | ||||||
|  | 
 | ||||||
|  | clearStore | ||||||
|  | 
 | ||||||
|  | outPath=$(nix-build structured-attrs.nix --no-out-link) | ||||||
|  | 
 | ||||||
|  | [[ $(cat $outPath/foo) = bar ]] | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue