diff --git a/src/libstore/build/derivation-building-goal.cc b/src/libstore/build/derivation-building-goal.cc index e336eca67230..891038947452 100644 --- a/src/libstore/build/derivation-building-goal.cc +++ b/src/libstore/build/derivation-building-goal.cc @@ -232,6 +232,20 @@ struct LogFile ~LogFile(); }; +/** + * The local build limit that applies to `drv`. + * + * Builtin builders do no user work and cannot be offloaded, since their + * platform is `builtin`, which no machine advertises. So `max-jobs = 0`, which + * means "build everything remotely", must not make them unbuildable: `nix + * profile` and `nix-env` both need `builtin:buildenv` to run. + */ +size_t DerivationBuildingGoal::buildSlotLimit() const +{ + auto limit = worker.settings.maxBuildJobs.get(); + return drv->isBuiltin() ? std::max(1u, limit) : limit; +} + struct LocalBuildRejection { bool maxJobsZero = false; @@ -346,7 +360,7 @@ Goal::Co DerivationBuildingGoal::tryToBuild(StorePathSet inputPaths) checkPathValidity(initialOutputs); auto localBuildResult = [&]() -> std::variant { - bool maxJobsZero = worker.settings.maxBuildJobs.get() == 0; + bool maxJobsZero = buildSlotLimit() == 0; auto * localStoreP = dynamic_cast(&worker.store); if (!localStoreP) @@ -854,7 +868,7 @@ Goal::Co DerivationBuildingGoal::buildLocally( while (true) { unsigned int curBuilds = worker.getNrLocalBuilds(); - if (curBuilds >= worker.settings.maxBuildJobs) { + if (curBuilds >= buildSlotLimit()) { outputLocks.unlock(); co_await waitForBuildSlot(); co_return tryToBuild(std::move(inputPaths)); diff --git a/src/libstore/build/goal.cc b/src/libstore/build/goal.cc index 5b1be405d246..87ccbdb99fe3 100644 --- a/src/libstore/build/goal.cc +++ b/src/libstore/build/goal.cc @@ -16,6 +16,12 @@ void TimedOut::anchor() {} void Goal::anchor() {} +size_t Goal::buildSlotLimit() const +{ + assert(jobCategory() == JobCategory::Build); + return worker.settings.maxBuildJobs; +} + using Co = nix::Goal::Co; using promise_type = nix::Goal::promise_type; diff --git a/src/libstore/build/worker.cc b/src/libstore/build/worker.cc index 6ff1ef12f745..daae59282d3b 100644 --- a/src/libstore/build/worker.cc +++ b/src/libstore/build/worker.cc @@ -258,7 +258,7 @@ void Worker::waitForBuildSlot(GoalPtr goal) if (goal->jobCategory() == JobCategory::Substitution) return getNrSubstitutions() < settings.maxSubstitutionJobs; else - return getNrLocalBuilds() < settings.maxBuildJobs; + return getNrLocalBuilds() < goal->buildSlotLimit(); }(); if (slotAvailable) @@ -324,21 +324,29 @@ void Worker::run(const Goals & _topGoals) break; // stuff may have been cancelled } - auto wakeSlotWaiters = [this](WeakGoals & waiting, size_t running, size_t limit) { + auto wakeSlotWaiters = [this](WeakGoals & waiting, size_t running, auto getLimit) { auto it = waiting.begin(); - while (it != waiting.end() && running < limit) { + while (it != waiting.end()) { auto goal = it->lock(); - it = waiting.erase(it); - if (!goal) + if (!goal) { + it = waiting.erase(it); + continue; + } + if (running >= getLimit(*goal)) { + ++it; continue; + } + it = waiting.erase(it); wakeUp(goal); ++running; } }; + wakeSlotWaiters(wantingToSubstitute, getNrSubstitutions(), [&](const Goal &) { + return std::max(1, settings.maxSubstitutionJobs); + }); wakeSlotWaiters( - wantingToSubstitute, getNrSubstitutions(), std::max(1, settings.maxSubstitutionJobs)); - wakeSlotWaiters(wantingToBuild, getNrLocalBuilds(), settings.maxBuildJobs); + wantingToBuild, getNrLocalBuilds(), [&](const Goal & goal) { return goal.buildSlotLimit(); }); } if (topGoals.empty()) diff --git a/src/libstore/builtins/buildenv.cc b/src/libstore/builtins/buildenv.cc index 2ad98fa92741..32139172962c 100644 --- a/src/libstore/builtins/buildenv.cc +++ b/src/libstore/builtins/buildenv.cc @@ -2,6 +2,7 @@ #include "nix/store/builtins.hh" #include "nix/store/derivations.hh" #include "nix/util/signals.hh" +#include "nix/util/util.hh" #include #include @@ -12,6 +13,52 @@ namespace nix { void BuildEnvFileConflictError::anchor() {} +std::string encodeBuildenvPackages(const Packages & pkgs) +{ + std::string res; + for (auto & pkg : pkgs) { + if (!res.empty()) + res += ' '; + /* A record is `active priority numOutputs path...`; we emit one output + per record, which decodes to the same flat list of packages. */ + res += fmt("%s %d 1 %s", pkg.active ? "true" : "false", pkg.priority, pkg.path.string()); + } + return res; +} + +Packages decodeBuildenvPackages(std::string_view s) +{ + Packages pkgs; + + auto tokens = tokenizeString(s); + auto it = tokens.begin(); + + auto next = [&]() -> std::string { + if (it == tokens.end()) + throw Error("'derivations' attribute of a buildenv derivation ends unexpectedly"); + return std::move(*it++); + }; + + auto nextInt = [&]() { + auto tok = next(); + if (auto n = string2Int(tok)) + return *n; + throw Error("'derivations' attribute of a buildenv derivation has '%s' where a number was expected", tok); + }; + + while (it != tokens.end()) { + const bool active = next() != "false"; + const int priority = nextInt(); + const int outputs = nextInt(); + if (outputs < 0) + throw Error("'derivations' attribute of a buildenv derivation has a negative output count"); + for (int n = 0; n < outputs; n++) + pkgs.emplace_back(next(), active, priority); + } + + return pkgs; +} + RegisterBuiltinBuilder::BuiltinBuilders & RegisterBuiltinBuilder::builtinBuilders() { static RegisterBuiltinBuilder::BuiltinBuilders builders; @@ -182,28 +229,15 @@ static void builtinBuildenv(const BuiltinBuilderContext & ctx) auto out = ctx.outputs.at("out"); createDirs(out); - /* Convert the stuff we get from the environment back into a - * coherent data type. */ - Packages pkgs; - { - auto derivations = tokenizeString(getAttr("derivations")); - - auto itemIt = derivations.begin(); - while (itemIt != derivations.end()) { - /* !!! We're trusting the caller to structure derivations env var correctly */ - const bool active = "false" != *itemIt++; - const int priority = stoi(*itemIt++); - const size_t outputs = stoul(*itemIt++); - - for (size_t n{0}; n < outputs; n++) { - pkgs.emplace_back(std::move(*itemIt++), active, priority); - } - } - } - - buildProfile(out, std::move(pkgs)); + buildProfile(out, decodeBuildenvPackages(getAttr("derivations"))); - createSymlink(getAttr("manifest"), out + "/manifest.nix"); + /* `nix profile` passes the manifest inline because it may be building + into a store that isn't mounted on this filesystem; `nix-env` passes + a store path to a Nix expression. Exactly one of the two is set. */ + if (auto manifestJSON = ctx.drv.env.find("manifestJSON"); manifestJSON != ctx.drv.env.end()) + writeFile(out + "/manifest.json", manifestJSON->second); + else + createSymlink(getAttr("manifest"), out + "/manifest.nix"); } static RegisterBuiltinBuilder registerBuildenv("buildenv", builtinBuildenv); diff --git a/src/libstore/include/nix/store/build/derivation-building-goal.hh b/src/libstore/include/nix/store/build/derivation-building-goal.hh index 08b3fa80d4b2..52f478d7fc78 100644 --- a/src/libstore/include/nix/store/build/derivation-building-goal.hh +++ b/src/libstore/include/nix/store/build/derivation-building-goal.hh @@ -116,6 +116,8 @@ private: { return JobCategory::Build; }; + + size_t buildSlotLimit() const override; }; } // namespace nix diff --git a/src/libstore/include/nix/store/build/goal.hh b/src/libstore/include/nix/store/build/goal.hh index f0861ddb4794..3af98bf46745 100644 --- a/src/libstore/include/nix/store/build/goal.hh +++ b/src/libstore/include/nix/store/build/goal.hh @@ -632,6 +632,15 @@ public: */ virtual JobCategory jobCategory() const = 0; + /** + * The number of build slots available to this goal. + * + * This is only meaningful for goals in the Build job category. Most build + * goals use the configured maximum directly, but goals that cannot be + * offloaded may override it. + */ + virtual size_t buildSlotLimit() const; + protected: Co await(Goals waitees); diff --git a/src/libstore/include/nix/store/builtins/buildenv.hh b/src/libstore/include/nix/store/builtins/buildenv.hh index b528871b2801..62aa4d90da75 100644 --- a/src/libstore/include/nix/store/builtins/buildenv.hh +++ b/src/libstore/include/nix/store/builtins/buildenv.hh @@ -23,6 +23,25 @@ struct Package } }; +typedef std::vector Packages; + +/** + * Encode `pkgs` for the `derivations` environment variable that + * `builtin:buildenv` reads. + * + * This lives next to `decodeBuildenvPackages` so that the two cannot drift. + * `src/nix/nix-env/buildenv.nix` produces the same format from the evaluator, + * and has to be kept in sync by hand. + */ +std::string encodeBuildenvPackages(const Packages & pkgs); + +/** + * Inverse of `encodeBuildenvPackages`. + * + * @throws Error if the encoding is malformed. + */ +Packages decodeBuildenvPackages(std::string_view s); + class BuildEnvFileConflictError final : public CloneableError { private: @@ -48,8 +67,6 @@ public: } }; -typedef std::vector Packages; - void buildProfile(const std::filesystem::path & out, Packages && pkgs); } // namespace nix diff --git a/src/nix/profile.cc b/src/nix/profile.cc index 011128872fe0..716b2abfff52 100644 --- a/src/nix/profile.cc +++ b/src/nix/profile.cc @@ -3,10 +3,10 @@ #include "nix/cmd/installable-flake.hh" #include "nix/main/common-args.hh" #include "nix/main/shared.hh" +#include "nix/store/build.hh" +#include "nix/store/builtins/buildenv.hh" #include "nix/store/store-api.hh" #include "nix/store/derivations.hh" -#include "nix/util/archive.hh" -#include "nix/store/builtins/buildenv.hh" #include "nix/flake/flakeref.hh" #include "nix-env/user-env.hh" #include "nix/store/profiles.hh" @@ -64,20 +64,6 @@ struct ProfileElement return dropEmptyInitThenConcatStringsSep(", ", names); } - /** - * Return a string representing an installable corresponding to the current - * element, either a flakeref or a plain store path - */ - StringSet toInstallables(Store & store) - { - if (source) - return {source->to_string()}; - StringSet rawPaths; - for (auto & path : storePaths) - rawPaths.insert(store.printStorePath(path)); - return rawPaths; - } - std::string versions() const { StringSet versions; @@ -121,12 +107,67 @@ struct ProfileManifest ProfileManifest() {} - ProfileManifest(EvalState & state, const std::filesystem::path & profile) + ProfileManifest(EvalState & state, ref store, const std::filesystem::path & profile) { auto manifestPath = profile / "manifest.json"; + std::optional manifestContents; + /* Where the manifest was actually read from, for error messages. */ + std::string manifestDesc = manifestPath.string(); + + auto profileStorePath = store->maybeParseStorePath(profile.string()); + if (!profileStorePath && std::filesystem::is_symlink(profile)) { + try { + profileStorePath = store->followLinksToStorePath(profile.string()); + } catch (BadStorePath &) { + /* Dangling, or resolving outside the store: not a store-backed + profile, so fall back to the host filesystem below. */ + } + } - if (std::filesystem::exists(manifestPath)) { - auto json = nlohmann::json::parse(readFile(manifestPath)); + /* Null when this store holds no such object, in which case the profile + may still be readable through the host filesystem. */ + std::shared_ptr accessor; + if (profileStorePath) + accessor = store->getFSAccessor(*profileStorePath); + + if (accessor) { + auto accessorPath = CanonPath("manifest.json"); + if (accessor->maybeLstat(accessorPath)) { + manifestContents = accessor->readFile(accessorPath); + manifestDesc = store->printStorePath(*profileStorePath) + "/manifest.json"; + } + } + + /* Older daemons only know the `manifest` buildenv attribute and + expose it as manifest.nix. New clients put the JSON manifest in + that store object as a compatibility fallback. */ + auto useCompatibilityManifest = [&](std::string contents, std::string desc) { + auto json = nlohmann::json::parse(contents, nullptr, false); + if (json.is_object() && json.contains("version") && json.contains("elements")) { + manifestContents = std::move(contents); + manifestDesc = std::move(desc); + } + }; + + if (!manifestContents && accessor) { + auto accessorPath = CanonPath("manifest.nix"); + if (auto stat = accessor->maybeLstat(accessorPath); stat && stat->type == SourceAccessor::tSymlink) { + auto compatibilityManifest = store->parseStorePath(accessor->readLink(accessorPath)); + auto compatibilityAccessor = store->requireStoreObjectAccessor(compatibilityManifest); + useCompatibilityManifest( + compatibilityAccessor->readFile(CanonPath::root), + store->printStorePath(*profileStorePath) + "/manifest.nix"); + } + } + + if (!manifestContents && !std::filesystem::exists(manifestPath)) { + auto compatibilityManifestPath = profile / "manifest.nix"; + if (std::filesystem::exists(compatibilityManifestPath)) + useCompatibilityManifest(readFile(compatibilityManifestPath), compatibilityManifestPath.string()); + } + + if (manifestContents || std::filesystem::exists(manifestPath)) { + auto json = nlohmann::json::parse(manifestContents ? *manifestContents : readFile(manifestPath)); auto version = json.value("version", 0); std::string sUrl; @@ -142,7 +183,7 @@ struct ProfileManifest sOriginalUrl = "originalUrl"; break; default: - throw Error("profile manifest %s has unsupported version %d", PathFmt(manifestPath), version); + throw Error("profile manifest '%s' has unsupported version %d", manifestDesc, version); } auto elems = json["elements"]; @@ -177,17 +218,33 @@ struct ProfileManifest } } - else if (std::filesystem::exists(profile / "manifest.nix")) { - // FIXME: needed because of pure mode; ugly. - state.allowPath(state.store->followLinksToStorePath(profile.string())); - state.allowPath(state.store->followLinksToStorePath((profile / "manifest.nix").string())); - - auto packageInfos = queryInstalled(state, state.store->followLinksToStore(profile.string())); - - for (auto & packageInfo : packageInfos) { - ProfileElement element; - element.storePaths = {packageInfo.queryOutPath()}; - addElement(std::move(element)); + /* Legacy `nix-env` profile. Reading one means evaluating its + `manifest.nix`, which the evaluator can only do through the host + filesystem, so a store that isn't mounted there can be detected but + not read. Detecting it still matters: silently reading such a profile + as empty would drop every installed package on the next write. */ + else { + auto hasLegacyManifest = accessor ? (bool) accessor->maybeLstat(CanonPath("manifest.nix")) + : std::filesystem::exists(profile / "manifest.nix"); + + if (hasLegacyManifest) { + if (!std::filesystem::exists(profile / "manifest.nix")) + throw Error( + "profile '%s' was created by 'nix-env', which can only read it from a store mounted at '%s'", + store->printStorePath(*profileStorePath), + store->storeDir); + + // FIXME: needed because of pure mode; ugly. + state.allowPath(state.store->followLinksToStorePath(profile.string())); + state.allowPath(state.store->followLinksToStorePath((profile / "manifest.nix").string())); + + auto packageInfos = queryInstalled(state, state.store->followLinksToStore(profile.string())); + + for (auto & packageInfo : packageInfos) { + ProfileElement element; + element.storePaths = {packageInfo.queryOutPath()}; + addElement(std::move(element)); + } } } } @@ -236,49 +293,54 @@ struct ProfileManifest StorePath build(ref store) { - auto tempDir = createTempDir(); - - StorePathSet references; - Packages pkgs; - for (auto & [name, element] : elements) { + StorePathSet packagePaths; + Derivation drv{ + .outputs = {{"out", DerivationOutput::Deferred{}}}, + .platform = "builtin", + .builder = "builtin:buildenv", + .env = + { + {"name", "profile"}, + {"out", ""}, + {"preferLocalBuild", "1"}, + {"allowSubstitutes", ""}, + }, + .name = "profile", + }; + + for (auto & [name, element] : elements) for (auto & path : element.storePaths) { - if (element.active) - pkgs.emplace_back(store->printStorePath(path), true, element.priority); - references.insert(path); + pkgs.emplace_back(store->printStorePath(path), element.active, element.priority); + packagePaths.insert(path); + drv.inputs.insert(SingleDerivedPath::Opaque{path}); } - } - - buildProfile(tempDir, std::move(pkgs)); - - writeFile(tempDir / "manifest.json", toJSON(*store).dump()); - - /* Add the symlink tree to the store. */ - StringSink sink; - dumpPath(tempDir, sink); - auto narHash = hashString(HashAlgorithm::SHA256, sink.s); - - auto info = ValidPathInfo::makeFromCA( - *store, - "profile", - FixedOutputInfo{ - .method = FileIngestionMethod::NixArchive, - .hash = narHash, - .references = - { - .others = std::move(references), - // profiles never refer to themselves - .self = false, - }, - }, - narHash); - info.narSize = sink.s.size(); - - StringSource source(sink.s); - store->addToStore(info, source); - - return std::move(info.path); + auto manifestJSON = toJSON(*store).dump(); + StringSource manifestSource{manifestJSON}; + auto compatibilityManifest = store->addToStoreFromDump( + manifestSource, + "profile-manifest.json", + FileSerialisationMethod::Flat, + ContentAddressMethod::Raw::Text, + HashAlgorithm::SHA256, + packagePaths); + + drv.env.insert_or_assign("derivations", encodeBuildenvPackages(pkgs)); + drv.env.insert_or_assign("manifest", store->printStorePath(compatibilityManifest)); + drv.env.insert_or_assign("manifestJSON", std::move(manifestJSON)); + drv.inputs.insert(SingleDerivedPath::Opaque{compatibilityManifest}); + fillInOutputPaths(drv, *store); + + auto outputPath = std::get(drv.outputs.at("out").raw).path; + auto drvPath = store->writeDerivation(drv); + + store->getBuilder()->buildPaths({DerivedPath::Built{ + .drvPath = makeConstantStorePathRef(drvPath), + .outputs = OutputsSpec::All{}, + }}); + + return outputPath; } static void printDiff(const ProfileManifest & prev, const ProfileManifest & cur, std::string_view indent) @@ -363,7 +425,7 @@ struct CmdProfileAdd : InstallablesCommand, MixDefaultProfile void run(ref store, Installables && installables) override { - ProfileManifest manifest(*getEvalState(), *profile); + ProfileManifest manifest(*getEvalState(), store, *profile); auto builtPaths = builtPathsPerInstallable( Installable::build2(getEvalStore(), store, Realise::Outputs, installables, bmNormal)); @@ -413,65 +475,7 @@ struct CmdProfileAdd : InstallablesCommand, MixDefaultProfile manifest.addElement(elementName, std::move(element)); } - try { - updateProfile(*store, manifest.build(store)); - } catch (BuildEnvFileConflictError & conflictError) { - // FIXME use C++20 std::ranges once macOS has it - // See - // https://github.com/NixOS/nix/compare/3efa476c5439f8f6c1968a6ba20a31d1239c2f04..1fe5d172ece51a619e879c4b86f603d9495cc102 - auto findRefByFilePath = [&](Iterator begin, Iterator end) { - for (auto it = begin; it != end; it++) { - auto & [name, profileElement] = *it; - for (auto & storePath : profileElement.storePaths) { - if (conflictError.fileA.string().starts_with(store->printStorePath(storePath))) { - return std::tuple(conflictError.fileA, name, profileElement.toInstallables(*store)); - } - if (conflictError.fileB.string().starts_with(store->printStorePath(storePath))) { - return std::tuple(conflictError.fileB, name, profileElement.toInstallables(*store)); - } - } - } - throw conflictError; - }; - // There are 2 conflicting files. We need to find out which one is from the already installed package and - // which one is the package that is the new package that is being installed. - // The first matching package is the one that was already installed (original). - auto [originalConflictingFilePath, originalEntryName, originalConflictingRefs] = - findRefByFilePath(manifest.elements.begin(), manifest.elements.end()); - // The last matching package is the one that was going to be installed (new). - auto [newConflictingFilePath, newEntryName, newConflictingRefs] = - findRefByFilePath(manifest.elements.rbegin(), manifest.elements.rend()); - - throw Error( - "An existing package already provides the following file:\n" - "\n" - " %1%\n" - "\n" - "This is the conflicting file from the new package:\n" - "\n" - " %2%\n" - "\n" - "To remove the existing package:\n" - "\n" - " nix profile remove %3%\n" - "\n" - "The new package can also be added next to the existing one by assigning a different priority.\n" - "The conflicting packages have a priority of %5%.\n" - "To prioritise the new package:\n" - "\n" - " nix profile add %4% --priority %6%\n" - "\n" - "To prioritise the existing package:\n" - "\n" - " nix profile add %4% --priority %7%\n", - PathFmt(originalConflictingFilePath), - PathFmt(newConflictingFilePath), - originalEntryName, - concatStringsSep(" ", newConflictingRefs), - conflictError.priority, - conflictError.priority - 1, - conflictError.priority + 1); - } + updateProfile(*store, manifest.build(store)); } }; @@ -571,7 +575,7 @@ class MixProfileElementMatchers : virtual Args, virtual StoreCommand, public vir return; auto evalState = evalCmd->getEvalState(); - ProfileManifest manifest(*evalState, *profile); + ProfileManifest manifest(*evalState, getStore(), *profile); for (auto & [name, element] : manifest.elements) if (name.starts_with(prefix)) @@ -667,7 +671,7 @@ struct CmdProfileRemove : virtual EvalCommand, MixProfileElementMatchers void run(ref store) override { - ProfileManifest oldManifest(*getEvalState(), *profile); + ProfileManifest oldManifest(*getEvalState(), store, *profile); ProfileManifest newManifest = oldManifest; @@ -708,7 +712,7 @@ struct CmdProfileUpgrade : virtual SourceExprCommand, MixProfileElementMatchers, void run(ref store) override { fetchSettings.tarballTtl = 0; - ProfileManifest manifest(*getEvalState(), *profile); + ProfileManifest manifest(*getEvalState(), store, *profile); Installables installables; std::vector elems; @@ -818,7 +822,7 @@ struct CmdProfileList : virtual EvalCommand, virtual StoreCommand, MixDefaultPro void run(ref store) override { - ProfileManifest manifest(*getEvalState(), *profile); + ProfileManifest manifest(*getEvalState(), store, *profile); if (json) { printJSON(manifest.toJSON(*store)); @@ -905,7 +909,13 @@ struct CmdProfileHistory : virtual StoreCommand, EvalCommand, MixDefaultProfile bool first = true; for (auto & gen : gens) { - ProfileManifest manifest(*getEvalState(), gen.path); + std::optional manifest; + try { + manifest.emplace(*getEvalState(), store, gen.path); + } catch (Error & e) { + warn("cannot read profile version %d: %s", gen.number, e.message()); + continue; + } if (!first) logger->cout(""); @@ -918,9 +928,9 @@ struct CmdProfileHistory : virtual StoreCommand, EvalCommand, MixDefaultProfile std::put_time(std::gmtime(&gen.creationTime), "%Y-%m-%d"), prevGen ? fmt(" <- %d", prevGen->first.number) : ""); - ProfileManifest::printDiff(prevGen ? prevGen->second : ProfileManifest(), manifest, " "); + ProfileManifest::printDiff(prevGen ? prevGen->second : ProfileManifest(), *manifest, " "); - prevGen = {gen, std::move(manifest)}; + prevGen = {gen, std::move(*manifest)}; } } }; diff --git a/tests/functional/nix-profile.sh b/tests/functional/nix-profile.sh index 556754a5338f..e71596014555 100755 --- a/tests/functional/nix-profile.sh +++ b/tests/functional/nix-profile.sh @@ -217,37 +217,9 @@ printf World2 > "$flake2Dir"/who nix profile add "$flake1Dir" [[ $("$TEST_HOME"/.nix-profile/bin/hello) = "Hello World" ]] -expect 1 nix profile add "$flake2Dir" -diff -u <( - nix --offline profile install "$flake2Dir" 2>&1 1> /dev/null \ - | grep -vE "^warning: " \ - | grep -vE "^error \(ignored\): " \ - | grep -vE "^waiting for " \ - || true -) <(cat << EOF -error: An existing package already provides the following file: - - "$(nix build --no-link --print-out-paths "${flake1Dir}""#default.out")/bin/hello" - - This is the conflicting file from the new package: - - "$(nix build --no-link --print-out-paths "${flake2Dir}""#default.out")/bin/hello" - - To remove the existing package: - - nix profile remove flake1 - - The new package can also be added next to the existing one by assigning a different priority. - The conflicting packages have a priority of 5. - To prioritise the new package: - - nix profile add path:${flake2Dir}#packages.${system}.default --priority 4 - - To prioritise the existing package: - - nix profile add path:${flake2Dir}#packages.${system}.default --priority 6 -EOF -) +expected=100 +if [[ -v NIX_DAEMON_PACKAGE ]]; then expected=1; fi # work around the daemon not returning a 100 status correctly +expect "$expected" nix profile add "$flake2Dir" [[ $("$TEST_HOME"/.nix-profile/bin/hello) = "Hello World" ]] nix profile add "$flake2Dir" --priority 100 [[ $("$TEST_HOME"/.nix-profile/bin/hello) = "Hello World" ]] @@ -262,7 +234,7 @@ nix profile add "$flake2Dir" --priority 0 clearProfiles # shellcheck disable=SC2046 nix profile add $(nix build "$flake1Dir" --no-link --print-out-paths) -expect 1 nix profile add --impure --expr "(builtins.getFlake ''$flake2Dir'').packages.$system.default" +expect "$expected" nix profile add --impure --expr "(builtins.getFlake ''$flake2Dir'').packages.$system.default" # Test upgrading from profile version 2. clearProfiles @@ -272,3 +244,74 @@ printf '{ "version": 2, "elements": [ { "active": true, "attrPath": "legacyPacka nix build --profile "$TEST_HOME"/.nix-profile "$(nix store add-path "$TEST_ROOT"/import-profile)" --no-link nix profile list | grep -A4 'Name:.*hello' | grep "Store paths:.*$outPath" nix profile remove hello 2>&1 | grep 'removed 1 packages, kept 0 packages' + +# A profile symlink that doesn't resolve into the store isn't a store-backed +# profile. Reading one must produce an empty manifest rather than an error, and +# `add` must be able to start a fresh generation over it. +danglingProfile=$TEST_ROOT/dangling-profile +ln -sfn "$TEST_ROOT/no-such-generation" "$danglingProfile" +nix profile list --profile "$danglingProfile" --json | jq -e '.elements == {}' +mkdir -p "$TEST_ROOT/plain-profile-target" +ln -sfn "$TEST_ROOT/plain-profile-target" "$danglingProfile" +nix profile list --profile "$danglingProfile" --json | jq -e '.elements == {}' +rm "$danglingProfile" + +# Building the profile is an implementation detail of `nix profile`, so it must +# not need a build slot: `max-jobs = 0` means "build everything remotely", and no +# remote builder can run a `builtin:` builder. This is daemon-side behavior, so +# an older daemon used by the compatibility suite cannot exercise it. +if [[ -z "${NIX_DAEMON_PACKAGE-}" ]]; then + clearProfiles + nix profile add --max-jobs 0 "$(nix build "${flake1Dir}^out" --no-link --print-out-paths)" + [[ $("$TEST_HOME"/.nix-profile/bin/hello) = "Hello World" ]] + + # A builtin waiting behind another builtin must be woken when the first + # releases the synthetic slot. + nix build --max-jobs 0 --no-link --expr ' + let + makeBuildenv = name: builtins.derivation { + inherit name; + system = "builtin"; + builder = "builtin:buildenv"; + derivations = ""; + manifestJSON = "{}"; + preferLocalBuild = true; + allowSubstitutes = false; + }; + in [ (makeBuildenv "builtin-slot-a") (makeBuildenv "builtin-slot-b") ] + ' +fi + +# Profiles must use the store API rather than assuming that logical store paths +# are directly accessible through the host filesystem. A rooted local store +# keeps its objects under $rootedStoreRoot while exposing them as /nix/store. +# +# Building into a relocated store is always sandboxed (see `isRelocatedStore` in +# derivation-builder.cc), which only Linux and FreeBSD implement and which needs +# working user namespaces. `canUseSandbox` covers both. +if canUseSandbox; then + rootedStoreRoot="$TEST_ROOT/rooted-store" + rootedStore="local?root=$rootedStoreRoot&store=/nix/store" + rootedProfile="$TEST_ROOT/rooted-profile" + rootedPackageDir="$TEST_ROOT/rooted-package" + mkdir -p "$rootedPackageDir/bin" + echo rooted > "$rootedPackageDir/bin/rooted-hello" + + rootedPackage=$(nix store add-path --store "$rootedStore" "$rootedPackageDir") + nix profile add --store "$rootedStore" --profile "$rootedProfile" "$rootedPackage" + nix profile list --store "$rootedStore" --profile "$rootedProfile" --json \ + | jq -e --arg path "$rootedPackage" '.elements | length == 1 and .[].storePaths == [$path]' + + rootedProfileGeneration=$(readlink "$rootedProfile") + rootedProfileStorePath=$(readlink "$(dirname "$rootedProfile")/$rootedProfileGeneration") + nix store cat --store "$rootedStore" "$rootedProfileStorePath/manifest.json" \ + | jq -e --arg path "$rootedPackage" '.elements | length == 1 and .[].storePaths == [$path]' + + # The profile has to hold the symlink tree too, not just the manifest. + nix store ls -l --store "$rootedStore" "$rootedProfileStorePath" \ + | grepQuiet "bin -> $rootedPackage/bin" + + nix profile remove --store "$rootedStore" --profile "$rootedProfile" --all + nix profile list --store "$rootedStore" --profile "$rootedProfile" --json \ + | jq -e '.elements == {}' +fi