Skip to content

Commit 647291c

Browse files
committed
Add basic impure derivations
Impure derivations are derivations that can produce a different result every time they're built. Example: stdenv.mkDerivation { name = "impure"; __impure = true; # marks this derivation as impure buildCommand = "date > $out"; }; Some important characteristics: * Impure derivations are not "cached". Thus, running "nix-build" on the example above multiple times will cause a rebuild every time. In the future, we could implement some mechanism for reusing impure builds across invocations. * The outputs of impure derivations are moved to a content-addressed location after the build (i.e., the resulting store path will correspond to the hash of the contents of the path). This way, multiple builds of the same impure derivation do not collide. * Because of content-addressability, the output paths of an impure derivation recorded in its .drv file are "virtual" placeholders for the actual outputs which are not known in advance. This also means that "nix-store -q bla.drv" gives a meaningless path. * Pure derivations are not allowed to depend on impure derivations. The only exception is fixed-output derivations. Because the latter always produce a known output, they can depend on impure shenanigans just fine. Also, repeatedly running "nix-build" on such a fixed-output derivation will *not* cause a rebuild of the impure dependency. After all, if the fixed output exists, its dependencies are no longer relevant. Thus, fixed-output derivations form an "impurity barrier" in the dependency graph. * When sandboxing is enabled, impure derivations can access the network in the same way as fixed-output derivations. In relaxed sandboxing mode, they can access the local filesystem. * Currently, the output of an impure derivation must have no references. This is because the content-addressing scheme must be extended to handle references, in particular self-references (as described in the ASE-2005 paper.) * Currently, impure derivations can only have a single output. No real reason for this. * "nix-build" on an impure derivation currently creates a result symlink to the incorrect, virtual output. A motivating example is the problem of using "fetchurl" on a dynamically generated tarball whose contents are deterministic, but where the tarball does not have a canonical form. Previously, this required "fetchurl" to do the unpacking in the same derivation. (That's what "fetchzip" does.) But now we can say: tarball = stdenv.mkDerivation { __impure = true; name = "tarball"; buildInputs = [ curl ]; buildCommand = "curl --fail -Lk https://github.com/NixOS/patchelf/tarball/c1f89c077e44a495c62ed0dcfaeca21510df93ef > $out"; }; unpacked = stdenv.mkDerivation { name = "unpacked"; outputHashAlgo = "sha256"; outputHashMode = "recursive"; outputHash = "1jl8n1n36w63wffkm56slcfa7vj9fxkv4ax0fr0mcfah55qj5l8s"; buildCommand = "mkdir $out; tar xvf ${tarball} -C $out"; }; I needed this because <nix/fetchurl.nix> does not support unpacking, and adding untar/unzip functionality would be annoying (especially since we can't just call "tar" or "unzip" in a sandbox). #520
1 parent 89ffe1e commit 647291c

File tree

3 files changed

+102
-30
lines changed

3 files changed

+102
-30
lines changed

src/libstore/build.cc

Lines changed: 90 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,10 @@ class Worker
245245
/* Cache for pathContentsGood(). */
246246
std::map<Path, bool> pathContentsGoodCache;
247247

248+
/* A mapping from the virtual output paths in impure derivations
249+
to the actual (content-addressed) resulting store paths. */
250+
std::map<Path, Path> impureRemapping;
251+
248252
public:
249253

250254
/* Set if at least one derivation had a BuildError (i.e. permanent
@@ -316,6 +320,17 @@ class Worker
316320
bool pathContentsGood(const Path & path);
317321

318322
void markContentsGood(const Path & path);
323+
324+
void remapImpureOutput(const Path & virtualOutput, const Path & actualOutput)
325+
{
326+
assert(virtualOutput.size() == actualOutput.size());
327+
impureRemapping[virtualOutput] = actualOutput;
328+
}
329+
330+
std::string remappedPath(const Path & path)
331+
{
332+
return get(impureRemapping, path, path);
333+
}
319334
};
320335

321336

@@ -760,9 +775,17 @@ class DerivationGoal : public Goal
760775
/* Whether this is a fixed-output derivation. */
761776
bool fixedOutput;
762777

778+
/* Whether this is an impure derivation. */
779+
bool isImpure = false;
780+
763781
/* Whether to run the build in a private network namespace. */
764782
bool privateNetwork = false;
765783

784+
bool allowNetwork()
785+
{
786+
return fixedOutput || isImpure;
787+
}
788+
766789
typedef void (DerivationGoal::*GoalState)();
767790
GoalState state;
768791

@@ -1061,12 +1084,21 @@ void DerivationGoal::haveDerivation()
10611084
{
10621085
trace("have derivation");
10631086

1087+
isImpure = drv->isImpure();
1088+
fixedOutput = drv->isFixedOutput();
1089+
1090+
if (isImpure && fixedOutput)
1091+
throw Error("derivation ‘%s’ cannot be both impure and fixed-output", drvPath);
1092+
10641093
for (auto & i : drv->outputs)
10651094
worker.store.addTempRoot(i.second.path);
10661095

10671096
/* Check what outputs paths are not already valid. */
10681097
PathSet invalidOutputs = checkPathValidity(false, buildMode == bmRepair);
10691098

1099+
if (isImpure && invalidOutputs.size() != wantedOutputs.size())
1100+
throw Error("derivation ‘%s’ is impure but some of its virtual outputs are valid", drvPath);
1101+
10701102
/* If they are all valid, then we're done. */
10711103
if (invalidOutputs.size() == 0 && buildMode == bmNormal) {
10721104
done(BuildResult::AlreadyValid);
@@ -1085,7 +1117,7 @@ void DerivationGoal::haveDerivation()
10851117
/* We are first going to try to create the invalid output paths
10861118
through substitutes. If that doesn't work, we'll build
10871119
them. */
1088-
if (settings.useSubstitutes && drv->substitutesAllowed())
1120+
if (settings.useSubstitutes && drv->substitutesAllowed() && !isImpure)
10891121
for (auto & i : invalidOutputs)
10901122
addWaitee(worker.makeSubstitutionGoal(i, buildMode == bmRepair));
10911123

@@ -1256,13 +1288,20 @@ void DerivationGoal::inputsRealised()
12561288
that are specified as inputs. */
12571289
assert(worker.store.isValidPath(i.first));
12581290
Derivation inDrv = worker.store.derivationFromPath(i.first);
1259-
for (auto & j : i.second)
1291+
for (auto & j : i.second) {
1292+
auto j2 = worker.remappedPath(inDrv.outputs[j].path);
1293+
if (j2 != inDrv.outputs[j].path)
1294+
inputRewrites[inDrv.outputs[j].path] = j2;
12601295
if (inDrv.outputs.find(j) != inDrv.outputs.end())
1261-
worker.store.computeFSClosure(inDrv.outputs[j].path, inputPaths);
1296+
worker.store.computeFSClosure(j2, inputPaths);
12621297
else
12631298
throw Error(
12641299
format("derivation ‘%1%’ requires non-existent output ‘%2%’ from input derivation ‘%3%’")
12651300
% drvPath % j % i.first);
1301+
}
1302+
1303+
if (!isImpure && !fixedOutput && inDrv.isImpure())
1304+
throw Error("pure derivation ‘%s’ depends on impure derivation ‘%s’", drvPath, i.first);
12661305
}
12671306

12681307
/* Second, the input sources. */
@@ -1272,14 +1311,10 @@ void DerivationGoal::inputsRealised()
12721311

12731312
allPaths.insert(inputPaths.begin(), inputPaths.end());
12741313

1275-
/* Is this a fixed-output derivation? */
1276-
fixedOutput = true;
1277-
for (auto & i : drv->outputs)
1278-
if (i.second.hash == "") fixedOutput = false;
1279-
12801314
/* Don't repeat fixed-output derivations since they're already
1281-
verified by their output hash.*/
1282-
nrRounds = fixedOutput ? 1 : settings.get("build-repeat", 0) + 1;
1315+
verified by their output hash. Similarly, don't repeat impure
1316+
derivations because by their nature they're not repeatable. */
1317+
nrRounds = fixedOutput || isImpure ? 1 : settings.get("build-repeat", 0) + 1;
12831318

12841319
/* Okay, try to build. Note that here we don't wait for a build
12851320
slot to become available, since we don't need one if there is a
@@ -1331,6 +1366,7 @@ void DerivationGoal::tryToBuild()
13311366
build this derivation, so no further checks are necessary. */
13321367
validPaths = checkPathValidity(true, buildMode == bmRepair);
13331368
if (buildMode != bmCheck && validPaths.size() == drv->outputs.size()) {
1369+
assert(!isImpure);
13341370
debug(format("skipping build of derivation ‘%1%’, someone beat us to it") % drvPath);
13351371
outputLocks.setDeletion(true);
13361372
done(BuildResult::AlreadyValid);
@@ -1345,7 +1381,10 @@ void DerivationGoal::tryToBuild()
13451381
them. */
13461382
for (auto & i : drv->outputs) {
13471383
Path path = i.second.path;
1348-
if (worker.store.isValidPath(path)) continue;
1384+
if (worker.store.isValidPath(path)) {
1385+
assert(!isImpure);
1386+
continue;
1387+
}
13491388
debug(format("removing invalid path ‘%1%’") % path);
13501389
deletePath(worker.store.toRealPath(path));
13511390
}
@@ -1561,7 +1600,7 @@ void DerivationGoal::buildDone()
15611600
st =
15621601
dynamic_cast<NotDeterministic*>(&e) ? BuildResult::NotDeterministic :
15631602
statusOk(status) ? BuildResult::OutputRejected :
1564-
fixedOutput || diskFull ? BuildResult::TransientFailure :
1603+
fixedOutput || isImpure || diskFull ? BuildResult::TransientFailure :
15651604
BuildResult::PermanentFailure;
15661605
}
15671606

@@ -1575,7 +1614,7 @@ void DerivationGoal::buildDone()
15751614

15761615
HookReply DerivationGoal::tryBuildHook()
15771616
{
1578-
if (!settings.useBuildHook || getEnv("NIX_BUILD_HOOK") == "" || !useDerivation) return rpDecline;
1617+
if (!settings.useBuildHook || getEnv("NIX_BUILD_HOOK") == "" || !useDerivation || isImpure) return rpDecline;
15791618

15801619
if (!worker.hook)
15811620
worker.hook = std::make_unique<HookInstance>();
@@ -1704,7 +1743,7 @@ void DerivationGoal::startBuilder()
17041743
else if (x == "false")
17051744
useChroot = false;
17061745
else if (x == "relaxed")
1707-
useChroot = !fixedOutput && get(drv->env, "__noChroot") != "1";
1746+
useChroot = !allowNetwork() && get(drv->env, "__noChroot") != "1";
17081747
}
17091748

17101749
if (worker.store.storeDir != worker.store.realStoreDir)
@@ -1862,7 +1901,7 @@ void DerivationGoal::startBuilder()
18621901
"nogroup:x:65534:\n") % sandboxGid).str());
18631902

18641903
/* Create /etc/hosts with localhost entry. */
1865-
if (!fixedOutput)
1904+
if (!allowNetwork())
18661905
writeFile(chrootRootDir + "/etc/hosts", "127.0.0.1 localhost\n");
18671906

18681907
/* Make the closure of the inputs available in the chroot,
@@ -2034,7 +2073,7 @@ void DerivationGoal::startBuilder()
20342073
us.
20352074
*/
20362075

2037-
if (!fixedOutput)
2076+
if (!allowNetwork())
20382077
privateNetwork = true;
20392078

20402079
userNamespaceSync.create();
@@ -2207,7 +2246,7 @@ void DerivationGoal::initEnv()
22072246
to the builder is generally impure, but the output of
22082247
fixed-output derivations is by definition pure (since we
22092248
already know the cryptographic hash of the output). */
2210-
if (fixedOutput) {
2249+
if (allowNetwork()) {
22112250
Strings varNames = tokenizeString<Strings>(get(drv->env, "impureEnvVars"));
22122251
for (auto & i : varNames) env[i] = getEnv(i);
22132252
}
@@ -2390,7 +2429,7 @@ void DerivationGoal::runChild()
23902429
/* Fixed-output derivations typically need to access the
23912430
network, so give them access to /etc/resolv.conf and so
23922431
on. */
2393-
if (fixedOutput) {
2432+
if (allowNetwork()) {
23942433
ss.push_back("/etc/resolv.conf");
23952434
ss.push_back("/etc/nsswitch.conf");
23962435
ss.push_back("/etc/services");
@@ -2725,10 +2764,13 @@ PathSet parseReferenceSpecifiers(Store & store, const BasicDerivation & drv, str
27252764

27262765
void DerivationGoal::registerOutputs()
27272766
{
2767+
// FIXME: This function is way to complicated.
2768+
27282769
/* When using a build hook, the build hook can register the output
27292770
as valid (by doing `nix-store --import'). If so we don't have
27302771
to do anything here. */
27312772
if (hook) {
2773+
assert(!isImpure);
27322774
bool allValid = true;
27332775
for (auto & i : drv->outputs)
27342776
if (!worker.store.isValidPath(i.second.path)) allValid = false;
@@ -2820,7 +2862,8 @@ void DerivationGoal::registerOutputs()
28202862
/* Check that fixed-output derivations produced the right
28212863
outputs (i.e., the content hash should match the specified
28222864
hash). */
2823-
if (i.second.hash != "") {
2865+
if (fixedOutput) {
2866+
assert(i.second.hash != "");
28242867

28252868
bool recursive; Hash h;
28262869
i.second.parseHashInfo(recursive, h);
@@ -2841,7 +2884,7 @@ void DerivationGoal::registerOutputs()
28412884
printError(format("build produced path ‘%1%’ with %2% hash ‘%3%’")
28422885
% dest % printHashType(h.type) % printHash16or32(h2));
28432886
if (worker.store.isValidPath(dest))
2844-
return;
2887+
continue;
28452888
Path actualDest = worker.store.toRealPath(dest);
28462889
if (actualPath != actualDest) {
28472890
PathLocks outputLocks({actualDest});
@@ -2901,16 +2944,6 @@ void DerivationGoal::registerOutputs()
29012944
continue;
29022945
}
29032946

2904-
/* For debugging, print out the referenced and unreferenced
2905-
paths. */
2906-
for (auto & i : inputPaths) {
2907-
PathSet::iterator j = references.find(i);
2908-
if (j == references.end())
2909-
debug(format("unreferenced input: ‘%1%’") % i);
2910-
else
2911-
debug(format("referenced input: ‘%1%’") % i);
2912-
}
2913-
29142947
/* Enforce `allowedReferences' and friends. */
29152948
auto checkRefs = [&](const string & attrName, bool allowed, bool recursive) {
29162949
if (drv->env.find(attrName) == drv->env.end()) return;
@@ -2953,6 +2986,33 @@ void DerivationGoal::registerOutputs()
29532986
checkRefs("disallowedReferences", false, false);
29542987
checkRefs("disallowedRequisites", false, true);
29552988

2989+
if (isImpure) {
2990+
2991+
/* Currently impure derivations cannot have any references. */
2992+
if (!references.empty())
2993+
throw BuildError("impure derivation output ‘%s’ has a reference to ‘%s’",
2994+
path, *references.begin());
2995+
2996+
/* Move the output to its content-addressed location. */
2997+
auto caPath = worker.store.makeFixedOutputPath(true, hash.first, storePathToName(path));
2998+
debug("moving impure output ‘%s’ to content-addressed ‘%s’", path, caPath);
2999+
3000+
worker.remapImpureOutput(path, caPath);
3001+
3002+
if (worker.store.isValidPath(caPath))
3003+
continue;
3004+
3005+
actualPath = worker.store.toRealPath(caPath);
3006+
deletePath(actualPath);
3007+
3008+
if (rename(path.c_str(), actualPath.c_str()))
3009+
throw SysError("moving ‘%s’ to ‘%s’", path, caPath);
3010+
3011+
path = caPath;
3012+
3013+
info.ca = makeFixedOutputCA(true, hash.first);
3014+
}
3015+
29563016
if (curRound == nrRounds) {
29573017
worker.store.optimisePath(actualPath); // FIXME: combine with scanForReferences()
29583018
worker.markContentsGood(path);

src/libstore/derivations.cc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,15 @@ bool BasicDerivation::isFixedOutput() const
309309
}
310310

311311

312+
bool BasicDerivation::isImpure() const
313+
{
314+
// FIXME: drop single output restriction
315+
return outputs.size() == 1 &&
316+
outputs.begin()->first == "out" &&
317+
get(env, "__impure", "") == "1";
318+
}
319+
320+
312321
DrvHashes drvHashes;
313322

314323

src/libstore/derivations.hh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ struct BasicDerivation
6666
/* Return true iff this is a fixed-output derivation. */
6767
bool isFixedOutput() const;
6868

69+
/* Return true iff this is an impure derivation. */
70+
bool isImpure() const;
71+
6972
/* Return the output paths of a derivation. */
7073
PathSet outputPaths() const;
7174

0 commit comments

Comments
 (0)