Skip to content

Commit fa4bd39

Browse files
authored
Merge pull request NixOS#11701 from DeterminateSystems/flake-substitution
Restore input substitution
2 parents 17c94b7 + 036359a commit fa4bd39

File tree

13 files changed

+203
-66
lines changed

13 files changed

+203
-66
lines changed

doc/manual/source/release-notes/rl-2.25.md

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,6 @@
7070

7171
Author: [**@zimbatm**](https://github.com/zimbatm)
7272

73-
- Flakes are no longer substituted [#10612](https://github.com/NixOS/nix/pull/10612)
74-
75-
Nix will no longer attempt to substitute the source code of flakes from a binary cache. This functionality was broken because it could lead to different evaluation results depending on whether the flake was available in the binary cache, or even depending on whether the flake was already in the local store.
76-
77-
Author: [**@edolstra**](https://github.com/edolstra)
78-
7973
- `<nix/fetchurl.nix>` uses TLS verification [#11585](https://github.com/NixOS/nix/pull/11585)
8074

8175
Previously `<nix/fetchurl.nix>` did not do TLS verification. This was because the Nix sandbox in the past did not have access to TLS certificates, and Nix checks the hash of the fetched file anyway. However, this can expose authentication data from `netrc` and URLs to man-in-the-middle attackers. In addition, Nix now in some cases (such as when using impure derivations) does *not* check the hash. Therefore we have now enabled TLS verification. This means that downloads by `<nix/fetchurl.nix>` will now fail if you're fetching from a HTTPS server that does not have a valid certificate.

src/libexpr/call-flake.nix

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ lockFileStr:
1010
# unlocked trees.
1111
overrides:
1212

13+
# This is `prim_fetchFinalTree`.
14+
fetchTreeFinal:
15+
1316
let
1417

1518
lockFile = builtins.fromJSON lockFileStr;
@@ -44,7 +47,8 @@ let
4447
overrides.${key}.sourceInfo
4548
else
4649
# FIXME: remove obsolete node.info.
47-
fetchTree (node.info or {} // removeAttrs node.locked ["dir"]);
50+
# Note: lock file entries are always final.
51+
fetchTreeFinal (node.info or {} // removeAttrs node.locked ["dir"]);
4852

4953
subdir = overrides.${key}.dir or node.locked.dir or "";
5054

src/libexpr/eval.cc

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -510,9 +510,15 @@ Value * EvalState::addPrimOp(PrimOp && primOp)
510510

511511
Value * v = allocValue();
512512
v->mkPrimOp(new PrimOp(primOp));
513-
staticBaseEnv->vars.emplace_back(envName, baseEnvDispl);
514-
baseEnv.values[baseEnvDispl++] = v;
515-
baseEnv.values[0]->payload.attrs->push_back(Attr(symbols.create(primOp.name), v));
513+
514+
if (primOp.internal)
515+
internalPrimOps.emplace(primOp.name, v);
516+
else {
517+
staticBaseEnv->vars.emplace_back(envName, baseEnvDispl);
518+
baseEnv.values[baseEnvDispl++] = v;
519+
baseEnv.values[0]->payload.attrs->push_back(Attr(symbols.create(primOp.name), v));
520+
}
521+
516522
return v;
517523
}
518524

src/libexpr/eval.hh

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,11 @@ struct PrimOp
107107
*/
108108
std::optional<ExperimentalFeature> experimentalFeature;
109109

110+
/**
111+
* If true, this primop is not exposed to the user.
112+
*/
113+
bool internal = false;
114+
110115
/**
111116
* Validity check to be performed by functions that introduce primops,
112117
* such as RegisterPrimOp() and Value::mkPrimOp().
@@ -591,6 +596,11 @@ public:
591596
*/
592597
std::shared_ptr<StaticEnv> staticBaseEnv; // !!! should be private
593598

599+
/**
600+
* Internal primops not exposed to the user.
601+
*/
602+
std::unordered_map<std::string, Value *, std::hash<std::string>, std::equal_to<std::string>, traceable_allocator<std::pair<const std::string, Value *>>> internalPrimOps;
603+
594604
/**
595605
* Name and documentation about every constant.
596606
*

src/libexpr/primops/fetchTree.cc

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ struct FetchTreeParams {
7878
bool emptyRevFallback = false;
7979
bool allowNameArgument = false;
8080
bool isFetchGit = false;
81+
bool isFinal = false;
8182
};
8283

8384
static void fetchTree(
@@ -195,6 +196,13 @@ static void fetchTree(
195196

196197
state.checkURI(input.toURLString());
197198

199+
if (params.isFinal) {
200+
input.attrs.insert_or_assign("__final", Explicit<bool>(true));
201+
} else {
202+
if (input.isFinal())
203+
throw Error("input '%s' is not allowed to use the '__final' attribute", input.to_string());
204+
}
205+
198206
auto [storePath, input2] = input.fetchToStore(state.store);
199207

200208
state.allowPath(storePath);
@@ -431,6 +439,18 @@ static RegisterPrimOp primop_fetchTree({
431439
.experimentalFeature = Xp::FetchTree,
432440
});
433441

442+
void prim_fetchFinalTree(EvalState & state, const PosIdx pos, Value * * args, Value & v)
443+
{
444+
fetchTree(state, pos, args, v, {.isFinal = true});
445+
}
446+
447+
static RegisterPrimOp primop_fetchFinalTree({
448+
.name = "fetchFinalTree",
449+
.args = {"input"},
450+
.fun = prim_fetchFinalTree,
451+
.internal = true,
452+
});
453+
434454
static void fetch(EvalState & state, const PosIdx pos, Value * * args, Value & v,
435455
const std::string & who, bool unpack, std::string name)
436456
{

src/libfetchers/fetchers.cc

Lines changed: 95 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include "source-path.hh"
44
#include "fetch-to-store.hh"
55
#include "json-utils.hh"
6+
#include "store-path-accessor.hh"
67

78
#include <nlohmann/json.hpp>
89

@@ -100,7 +101,7 @@ Input Input::fromAttrs(const Settings & settings, Attrs && attrs)
100101
auto allowedAttrs = inputScheme->allowedAttrs();
101102

102103
for (auto & [name, _] : attrs)
103-
if (name != "type" && allowedAttrs.count(name) == 0)
104+
if (name != "type" && name != "__final" && allowedAttrs.count(name) == 0)
104105
throw Error("input attribute '%s' not supported by scheme '%s'", name, schemeName);
105106

106107
auto res = inputScheme->inputFromAttrs(settings, attrs);
@@ -145,6 +146,11 @@ bool Input::isLocked() const
145146
return scheme && scheme->isLocked(*this);
146147
}
147148

149+
bool Input::isFinal() const
150+
{
151+
return maybeGetBoolAttr(attrs, "__final").value_or(false);
152+
}
153+
148154
Attrs Input::toAttrs() const
149155
{
150156
return attrs;
@@ -172,16 +178,24 @@ std::pair<StorePath, Input> Input::fetchToStore(ref<Store> store) const
172178

173179
auto [storePath, input] = [&]() -> std::pair<StorePath, Input> {
174180
try {
175-
auto [accessor, final] = getAccessorUnchecked(store);
181+
auto [accessor, result] = getAccessorUnchecked(store);
176182

177-
auto storePath = nix::fetchToStore(*store, SourcePath(accessor), FetchMode::Copy, final.getName());
183+
auto storePath = nix::fetchToStore(*store, SourcePath(accessor), FetchMode::Copy, result.getName());
178184

179185
auto narHash = store->queryPathInfo(storePath)->narHash;
180-
final.attrs.insert_or_assign("narHash", narHash.to_string(HashFormat::SRI, true));
186+
result.attrs.insert_or_assign("narHash", narHash.to_string(HashFormat::SRI, true));
181187

182-
scheme->checkLocks(*this, final);
188+
// FIXME: we would like to mark inputs as final in
189+
// getAccessorUnchecked(), but then we can't add
190+
// narHash. Or maybe narHash should be excluded from the
191+
// concept of "final" inputs?
192+
result.attrs.insert_or_assign("__final", Explicit<bool>(true));
183193

184-
return {storePath, final};
194+
assert(result.isFinal());
195+
196+
checkLocks(*this, result);
197+
198+
return {storePath, result};
185199
} catch (Error & e) {
186200
e.addTrace({}, "while fetching the input '%s'", to_string());
187201
throw;
@@ -191,46 +205,73 @@ std::pair<StorePath, Input> Input::fetchToStore(ref<Store> store) const
191205
return {std::move(storePath), input};
192206
}
193207

194-
void InputScheme::checkLocks(const Input & specified, const Input & final) const
195-
{
208+
void Input::checkLocks(Input specified, Input & result)
209+
{
210+
/* If the original input is final, then we just return the
211+
original attributes, dropping any new fields returned by the
212+
fetcher. However, any fields that are in both the specified and
213+
result input must be identical. */
214+
if (specified.isFinal()) {
215+
216+
/* Backwards compatibility hack: we had some lock files in the
217+
past that 'narHash' fields with incorrect base-64
218+
formatting (lacking the trailing '=', e.g. 'sha256-ri...Mw'
219+
instead of ''sha256-ri...Mw='). So fix that. */
220+
if (auto prevNarHash = specified.getNarHash())
221+
specified.attrs.insert_or_assign("narHash", prevNarHash->to_string(HashFormat::SRI, true));
222+
223+
for (auto & field : specified.attrs) {
224+
auto field2 = result.attrs.find(field.first);
225+
if (field2 != result.attrs.end() && field.second != field2->second)
226+
throw Error("mismatch in field '%s' of input '%s', got '%s'",
227+
field.first,
228+
attrsToJSON(specified.attrs),
229+
attrsToJSON(result.attrs));
230+
}
231+
232+
result.attrs = specified.attrs;
233+
234+
return;
235+
}
236+
196237
if (auto prevNarHash = specified.getNarHash()) {
197-
if (final.getNarHash() != prevNarHash) {
198-
if (final.getNarHash())
238+
if (result.getNarHash() != prevNarHash) {
239+
if (result.getNarHash())
199240
throw Error((unsigned int) 102, "NAR hash mismatch in input '%s', expected '%s' but got '%s'",
200-
specified.to_string(), prevNarHash->to_string(HashFormat::SRI, true), final.getNarHash()->to_string(HashFormat::SRI, true));
241+
specified.to_string(), prevNarHash->to_string(HashFormat::SRI, true), result.getNarHash()->to_string(HashFormat::SRI, true));
201242
else
202243
throw Error((unsigned int) 102, "NAR hash mismatch in input '%s', expected '%s' but got none",
203244
specified.to_string(), prevNarHash->to_string(HashFormat::SRI, true));
204245
}
205246
}
206247

207248
if (auto prevLastModified = specified.getLastModified()) {
208-
if (final.getLastModified() != prevLastModified)
209-
throw Error("'lastModified' attribute mismatch in input '%s', expected %d",
210-
final.to_string(), *prevLastModified);
249+
if (result.getLastModified() != prevLastModified)
250+
throw Error("'lastModified' attribute mismatch in input '%s', expected %d, got %d",
251+
result.to_string(), *prevLastModified, result.getLastModified().value_or(-1));
211252
}
212253

213254
if (auto prevRev = specified.getRev()) {
214-
if (final.getRev() != prevRev)
255+
if (result.getRev() != prevRev)
215256
throw Error("'rev' attribute mismatch in input '%s', expected %s",
216-
final.to_string(), prevRev->gitRev());
257+
result.to_string(), prevRev->gitRev());
217258
}
218259

219260
if (auto prevRevCount = specified.getRevCount()) {
220-
if (final.getRevCount() != prevRevCount)
261+
if (result.getRevCount() != prevRevCount)
221262
throw Error("'revCount' attribute mismatch in input '%s', expected %d",
222-
final.to_string(), *prevRevCount);
263+
result.to_string(), *prevRevCount);
223264
}
224265
}
225266

226267
std::pair<ref<SourceAccessor>, Input> Input::getAccessor(ref<Store> store) const
227268
{
228269
try {
229-
auto [accessor, final] = getAccessorUnchecked(store);
270+
auto [accessor, result] = getAccessorUnchecked(store);
230271

231-
scheme->checkLocks(*this, final);
272+
checkLocks(*this, result);
232273

233-
return {accessor, std::move(final)};
274+
return {accessor, std::move(result)};
234275
} catch (Error & e) {
235276
e.addTrace({}, "while fetching the input '%s'", to_string());
236277
throw;
@@ -244,12 +285,42 @@ std::pair<ref<SourceAccessor>, Input> Input::getAccessorUnchecked(ref<Store> sto
244285
if (!scheme)
245286
throw Error("cannot fetch unsupported input '%s'", attrsToJSON(toAttrs()));
246287

247-
auto [accessor, final] = scheme->getAccessor(store, *this);
288+
/* The tree may already be in the Nix store, or it could be
289+
substituted (which is often faster than fetching from the
290+
original source). So check that. We only do this for final
291+
inputs, otherwise there is a risk that we don't return the
292+
same attributes (like `lastModified`) that the "real" fetcher
293+
would return.
294+
295+
FIXME: add a setting to disable this.
296+
FIXME: substituting may be slower than fetching normally,
297+
e.g. for fetchers like Git that are incremental!
298+
*/
299+
if (isFinal() && getNarHash()) {
300+
try {
301+
auto storePath = computeStorePath(*store);
302+
303+
store->ensurePath(storePath);
304+
305+
debug("using substituted/cached input '%s' in '%s'",
306+
to_string(), store->printStorePath(storePath));
307+
308+
auto accessor = makeStorePathAccessor(store, storePath);
309+
310+
accessor->fingerprint = scheme->getFingerprint(store, *this);
311+
312+
return {accessor, *this};
313+
} catch (Error & e) {
314+
debug("substitution of input '%s' failed: %s", to_string(), e.what());
315+
}
316+
}
317+
318+
auto [accessor, result] = scheme->getAccessor(store, *this);
248319

249320
assert(!accessor->fingerprint);
250-
accessor->fingerprint = scheme->getFingerprint(store, final);
321+
accessor->fingerprint = scheme->getFingerprint(store, result);
251322

252-
return {accessor, std::move(final)};
323+
return {accessor, std::move(result)};
253324
}
254325

255326
Input Input::applyOverrides(

0 commit comments

Comments
 (0)