nix copy: Add --profile flag

This allows `nix copy` to atomically copy a store path and point a
profile to it, without the risk that the store path might be GC'ed in
between. This is useful for instance when deploying a new NixOS system
profile from a remote store.
This commit is contained in:
Eelco Dolstra 2024-10-08 15:28:49 +02:00
parent 26c3fc11ea
commit 76f75e7691
9 changed files with 53 additions and 27 deletions

View File

@ -179,30 +179,34 @@ BuiltPathsCommand::BuiltPathsCommand(bool recursive)
void BuiltPathsCommand::run(ref<Store> store, Installables && installables) void BuiltPathsCommand::run(ref<Store> store, Installables && installables)
{ {
BuiltPaths paths; BuiltPaths rootPaths, allPaths;
if (all) { if (all) {
if (installables.size()) if (installables.size())
throw UsageError("'--all' does not expect arguments"); throw UsageError("'--all' does not expect arguments");
// XXX: Only uses opaque paths, ignores all the realisations // XXX: Only uses opaque paths, ignores all the realisations
for (auto & p : store->queryAllValidPaths()) for (auto & p : store->queryAllValidPaths())
paths.emplace_back(BuiltPath::Opaque{p}); rootPaths.emplace_back(BuiltPath::Opaque{p});
allPaths = rootPaths;
} else { } else {
paths = Installable::toBuiltPaths(getEvalStore(), store, realiseMode, operateOn, installables); rootPaths = Installable::toBuiltPaths(getEvalStore(), store, realiseMode, operateOn, installables);
allPaths = rootPaths;
if (recursive) { if (recursive) {
// XXX: This only computes the store path closure, ignoring // XXX: This only computes the store path closure, ignoring
// intermediate realisations // intermediate realisations
StorePathSet pathsRoots, pathsClosure; StorePathSet pathsRoots, pathsClosure;
for (auto & root : paths) { for (auto & root : rootPaths) {
auto rootFromThis = root.outPaths(); auto rootFromThis = root.outPaths();
pathsRoots.insert(rootFromThis.begin(), rootFromThis.end()); pathsRoots.insert(rootFromThis.begin(), rootFromThis.end());
} }
store->computeFSClosure(pathsRoots, pathsClosure); store->computeFSClosure(pathsRoots, pathsClosure);
for (auto & path : pathsClosure) for (auto & path : pathsClosure)
paths.emplace_back(BuiltPath::Opaque{path}); allPaths.emplace_back(BuiltPath::Opaque{path});
} }
} }
run(store, std::move(paths)); run(store, std::move(allPaths), std::move(rootPaths));
} }
StorePathsCommand::StorePathsCommand(bool recursive) StorePathsCommand::StorePathsCommand(bool recursive)
@ -210,10 +214,10 @@ StorePathsCommand::StorePathsCommand(bool recursive)
{ {
} }
void StorePathsCommand::run(ref<Store> store, BuiltPaths && paths) void StorePathsCommand::run(ref<Store> store, BuiltPaths && allPaths, BuiltPaths && rootPaths)
{ {
StorePathSet storePaths; StorePathSet storePaths;
for (auto & builtPath : paths) for (auto & builtPath : allPaths)
for (auto & p : builtPath.outPaths()) for (auto & p : builtPath.outPaths())
storePaths.insert(p); storePaths.insert(p);
@ -242,17 +246,21 @@ MixProfile::MixProfile()
}); });
} }
void MixProfile::updateProfile(const StorePath & storePath) void MixProfile::updateProfile(
const StorePath & storePath,
ref<Store> store_)
{ {
if (!profile) return; if (!profile) return;
auto store = getStore().dynamic_pointer_cast<LocalFSStore>(); auto store = store_.dynamic_pointer_cast<LocalFSStore>();
if (!store) throw Error("'--profile' is not supported for this Nix store"); if (!store) throw Error("'--profile' is not supported for this Nix store");
auto profile2 = absPath(*profile); auto profile2 = absPath(*profile);
switchLink(profile2, switchLink(profile2,
createGeneration(*store, profile2, storePath)); createGeneration(*store, profile2, storePath));
} }
void MixProfile::updateProfile(const BuiltPaths & buildables) void MixProfile::updateProfile(
const BuiltPaths & buildables,
ref<Store> store)
{ {
if (!profile) return; if (!profile) return;
@ -274,7 +282,7 @@ void MixProfile::updateProfile(const BuiltPaths & buildables)
if (result.size() != 1) if (result.size() != 1)
throw UsageError("'--profile' requires that the arguments produce a single store path, but there are %d", result.size()); throw UsageError("'--profile' requires that the arguments produce a single store path, but there are %d", result.size());
updateProfile(result[0]); updateProfile(result[0], store);
} }
MixDefaultProfile::MixDefaultProfile() MixDefaultProfile::MixDefaultProfile()
@ -308,7 +316,8 @@ MixEnvironment::MixEnvironment() : ignoreEnvironment(false)
}); });
} }
void MixEnvironment::setEnviron() { void MixEnvironment::setEnviron()
{
if (ignoreEnvironment) { if (ignoreEnvironment) {
if (!unset.empty()) if (!unset.empty())
throw UsageError("--unset does not make sense with --ignore-environment"); throw UsageError("--unset does not make sense with --ignore-environment");

View File

@ -238,7 +238,7 @@ public:
BuiltPathsCommand(bool recursive = false); BuiltPathsCommand(bool recursive = false);
virtual void run(ref<Store> store, BuiltPaths && paths) = 0; virtual void run(ref<Store> store, BuiltPaths && allPaths, BuiltPaths && rootPaths) = 0;
void run(ref<Store> store, Installables && installables) override; void run(ref<Store> store, Installables && installables) override;
@ -251,7 +251,7 @@ struct StorePathsCommand : public BuiltPathsCommand
virtual void run(ref<Store> store, StorePaths && storePaths) = 0; virtual void run(ref<Store> store, StorePaths && storePaths) = 0;
void run(ref<Store> store, BuiltPaths && paths) override; void run(ref<Store> store, BuiltPaths && allPaths, BuiltPaths && rootPaths) override;
}; };
/** /**
@ -301,11 +301,15 @@ struct MixProfile : virtual StoreCommand
MixProfile(); MixProfile();
/* If 'profile' is set, make it point at 'storePath'. */ /* If 'profile' is set, make it point at 'storePath'. */
void updateProfile(const StorePath & storePath); void updateProfile(
const StorePath & storePath,
ref<Store> store);
/* If 'profile' is set, make it point at the store path produced /* If 'profile' is set, make it point at the store path produced
by 'buildables'. */ by 'buildables'. */
void updateProfile(const BuiltPaths & buildables); void updateProfile(
const BuiltPaths & buildables,
ref<Store> store);
}; };
struct MixDefaultProfile : MixProfile struct MixDefaultProfile : MixProfile

View File

@ -161,7 +161,7 @@ struct CmdBuild : InstallablesCommand, MixDryRun, MixJSON, MixProfile
BuiltPaths buildables2; BuiltPaths buildables2;
for (auto & b : buildables) for (auto & b : buildables)
buildables2.push_back(b.path); buildables2.push_back(b.path);
updateProfile(buildables2); updateProfile(buildables2, store);
} }
}; };

View File

@ -4,7 +4,7 @@
using namespace nix; using namespace nix;
struct CmdCopy : virtual CopyCommand, virtual BuiltPathsCommand struct CmdCopy : virtual CopyCommand, virtual BuiltPathsCommand, MixProfile
{ {
CheckSigsFlag checkSigs = CheckSigs; CheckSigsFlag checkSigs = CheckSigs;
@ -43,19 +43,21 @@ struct CmdCopy : virtual CopyCommand, virtual BuiltPathsCommand
Category category() override { return catSecondary; } Category category() override { return catSecondary; }
void run(ref<Store> srcStore, BuiltPaths && paths) override void run(ref<Store> srcStore, BuiltPaths && allPaths, BuiltPaths && rootPaths) override
{ {
auto dstStore = getDstStore(); auto dstStore = getDstStore();
RealisedPath::Set stuffToCopy; RealisedPath::Set stuffToCopy;
for (auto & builtPath : paths) { for (auto & builtPath : allPaths) {
auto theseRealisations = builtPath.toRealisedPaths(*srcStore); auto theseRealisations = builtPath.toRealisedPaths(*srcStore);
stuffToCopy.insert(theseRealisations.begin(), theseRealisations.end()); stuffToCopy.insert(theseRealisations.begin(), theseRealisations.end());
} }
copyPaths( copyPaths(
*srcStore, *dstStore, stuffToCopy, NoRepair, checkSigs, substitute); *srcStore, *dstStore, stuffToCopy, NoRepair, checkSigs, substitute);
updateProfile(rootPaths, dstStore);
} }
}; };

View File

@ -55,6 +55,15 @@ R""(
# nix copy --to /tmp/nix nixpkgs#hello --no-check-sigs # nix copy --to /tmp/nix nixpkgs#hello --no-check-sigs
``` ```
* Update the NixOS system profile to point to a closure copied from a
remote machine:
```console
# nix copy --from ssh://server \
--profile /nix/var/nix/profiles/system \
/nix/store/r14v3km89zm3prwsa521fab5kgzvfbw4-nixos-system-foobar-24.05.20240925.759537f
```
# Description # Description
`nix copy` copies store path closures between two Nix stores. The `nix copy` copies store path closures between two Nix stores. The

View File

@ -502,7 +502,7 @@ struct Common : InstallableCommand, MixProfile
auto strPath = store->printStorePath(shellOutPath); auto strPath = store->printStorePath(shellOutPath);
updateProfile(shellOutPath); updateProfile(shellOutPath, store);
debug("reading environment file '%s'", strPath); debug("reading environment file '%s'", strPath);

View File

@ -424,7 +424,7 @@ struct CmdProfileInstall : InstallablesCommand, MixDefaultProfile
} }
try { try {
updateProfile(manifest.build(store)); updateProfile(manifest.build(store), store);
} catch (BuildEnvFileConflictError & conflictError) { } catch (BuildEnvFileConflictError & conflictError) {
// FIXME use C++20 std::ranges once macOS has it // FIXME use C++20 std::ranges once macOS has it
// See https://github.com/NixOS/nix/compare/3efa476c5439f8f6c1968a6ba20a31d1239c2f04..1fe5d172ece51a619e879c4b86f603d9495cc102 // See https://github.com/NixOS/nix/compare/3efa476c5439f8f6c1968a6ba20a31d1239c2f04..1fe5d172ece51a619e879c4b86f603d9495cc102
@ -669,7 +669,7 @@ struct CmdProfileRemove : virtual EvalCommand, MixDefaultProfile, MixProfileElem
removedCount, removedCount,
newManifest.elements.size()); newManifest.elements.size());
updateProfile(newManifest.build(store)); updateProfile(newManifest.build(store), store);
} }
}; };
@ -779,7 +779,7 @@ struct CmdProfileUpgrade : virtual SourceExprCommand, MixDefaultProfile, MixProf
builtPaths.find(&*installable)->second.first); builtPaths.find(&*installable)->second.first);
} }
updateProfile(manifest.build(store)); updateProfile(manifest.build(store), store);
} }
}; };

View File

@ -36,7 +36,7 @@ struct CmdRealisationInfo : BuiltPathsCommand, MixJSON
Category category() override { return catSecondary; } Category category() override { return catSecondary; }
void run(ref<Store> store, BuiltPaths && paths) override void run(ref<Store> store, BuiltPaths && paths, BuiltPaths && rootPaths) override
{ {
experimentalFeatureSettings.require(Xp::CaDerivations); experimentalFeatureSettings.require(Xp::CaDerivations);
RealisedPath::Set realisations; RealisedPath::Set realisations;

View File

@ -18,7 +18,9 @@ HASH=$(nix hash path $outPath)
clearStore clearStore
clearCacheCache clearCacheCache
nix copy --from $cacheURI $outPath --no-check-sigs nix copy --from $cacheURI $outPath --no-check-sigs --profile $TEST_ROOT/profile
[[ -e $TEST_ROOT/profile ]]
if ls $cacheDir/nar/*.zst &> /dev/null; then if ls $cacheDir/nar/*.zst &> /dev/null; then
echo "files do exist" echo "files do exist"