From 2a51236b810a202948906531bb07f1dff12a661d Mon Sep 17 00:00:00 2001 From: Jonathan Ringer Date: Mon, 23 Mar 2026 11:14:08 -0700 Subject: [PATCH] Ensure zone exists before attempting to read --- src/libexpr/eval.cc | 121 ++++++++++++++---- src/libexpr/include/nix/expr/eval-settings.hh | 9 ++ src/libexpr/include/nix/expr/eval.hh | 9 ++ 3 files changed, 117 insertions(+), 22 deletions(-) diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index d7c9f71cda86..d1d71f73c207 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -593,30 +593,33 @@ Hash EvalState::getWorldTreeSha(std::string_view worldPath) const return currentSha; } +std::filesystem::path EvalState::resolveGitDir() const +{ + auto checkoutPath = settings.tectonixCheckoutPath.get(); + auto dotGitPath = std::filesystem::path(checkoutPath) / ".git"; + + if (std::filesystem::is_directory(dotGitPath)) { + return dotGitPath; + } else if (std::filesystem::is_regular_file(dotGitPath)) { + auto gitdirContent = readFile(dotGitPath.string()); + // Parse "gitdir: \n" + if (hasPrefix(gitdirContent, "gitdir: ")) { + auto path = trim(gitdirContent.substr(8)); + auto gitDir = std::filesystem::path(path); + // Handle relative paths + if (gitDir.is_relative()) + gitDir = std::filesystem::path(checkoutPath) / gitDir; + return gitDir; + } + } + return {}; +} + const std::set & EvalState::getTectonixSparseCheckoutRoots() const { std::call_once(tectonixSparseCheckoutRootsFlag, [this]() { if (isTectonixSourceAvailable()) { - auto checkoutPath = settings.tectonixCheckoutPath.get(); - - // Read .git to find the actual git directory - // It can be either a directory or a file containing "gitdir: " - auto dotGitPath = std::filesystem::path(checkoutPath) / ".git"; - std::filesystem::path gitDir; - - if (std::filesystem::is_directory(dotGitPath)) { - gitDir = dotGitPath; - } else if (std::filesystem::is_regular_file(dotGitPath)) { - auto gitdirContent = readFile(dotGitPath.string()); - // Parse "gitdir: \n" - if (hasPrefix(gitdirContent, "gitdir: ")) { - auto path = trim(gitdirContent.substr(8)); - gitDir = std::filesystem::path(path); - // Handle relative paths - if (gitDir.is_relative()) - gitDir = std::filesystem::path(checkoutPath) / gitDir; - } - } + auto gitDir = resolveGitDir(); if (!gitDir.empty()) { // Read sparse-checkout-roots @@ -635,6 +638,69 @@ const std::set & EvalState::getTectonixSparseCheckoutRoots() const return tectonixSparseCheckoutRoots; } +bool EvalState::ensureZoneInSparseCheckout(std::string_view zonePath) +{ + if (!isTectonixSourceAvailable() || !settings.tectonixAutoSparseCheckout) + return false; + + auto zone = normalizeZonePath(zonePath); + auto checkoutPath = settings.tectonixCheckoutPath.get(); + auto fullPath = std::filesystem::path(checkoutPath) / zone; + + // Fast path: zone already on disk + if (std::filesystem::exists(fullPath)) + return false; + + std::lock_guard lock(sparseCheckoutMutex_); + + // Double-check after acquiring lock + if (std::filesystem::exists(fullPath)) + return false; + + // Look up zone ID from manifest + std::string zoneId; + try { + auto & manifest = getManifestJson(); + auto zoneKey = std::string(zonePath); + if (manifest.contains(zoneKey) && manifest.at(zoneKey).contains("id")) { + zoneId = manifest.at(zoneKey).at("id").get(); + } + } catch (...) { + warn("failed to look up zone ID for '%s' from manifest", zonePath); + return false; + } + + if (zoneId.empty()) { + debug("ensureZoneInSparseCheckout: no zone ID found for '%s'", zonePath); + return false; + } + + // Run git sparse-checkout add + // The zone path for sparse-checkout needs a leading / (e.g., /areas/tools/foo) + auto sparsePattern = "/" + zone; + try { + runProgram("git", true, {"-C", checkoutPath, "sparse-checkout", "add", sparsePattern}); + } catch (ExecError & e) { + warn("failed to add '%s' to sparse checkout: %s", zonePath, e.what()); + return false; + } + + // Append zone ID to sparse-checkout-roots + auto gitDir = resolveGitDir(); + if (!gitDir.empty()) { + auto sparseRootsPath = gitDir / "info" / "sparse-checkout-roots"; + std::ofstream ofs(sparseRootsPath, std::ios::app); + if (ofs) { + ofs << zoneId << "\n"; + } else { + warn("failed to append zone ID '%s' to %s", zoneId, sparseRootsPath.string()); + } + } + + printInfo("auto-added zone '%s' (id: %s) to sparse checkout", zonePath, zoneId); + return true; +} + const std::map & EvalState::getTectonixDirtyZones() const { std::call_once(tectonixDirtyZonesFlag, [this]() { @@ -786,6 +852,9 @@ const nlohmann::json & EvalState::getManifestJson() const StorePath EvalState::getZoneStorePath(std::string_view zonePath) { + // Auto-add zone to sparse checkout if not present on disk + ensureZoneInSparseCheckout(zonePath); + // Check dirty status using original zonePath (with // prefix) since // tectonixDirtyZones keys come directly from manifest with // prefix const ZoneDirtyInfo * dirtyInfo = nullptr; @@ -994,8 +1063,16 @@ StorePath EvalState::getZoneFromCheckout(std::string_view zonePath, const boost: auto it = cache->find(std::string(zonePath)); if (it != cache->end()) return it->second; - if (!std::filesystem::exists(fullPath)) - throw Error("zone '%s' not found in checkout at '%s'", zonePath, fullPath.string()); + if (!std::filesystem::exists(fullPath)) { + // Zone is in sparse-checkout-roots but files are missing; try to restore + try { + runProgram("git", true, {"-C", checkoutPath, "checkout", "HEAD", "--", zone}); + } catch (ExecError &) { + // Restore failed, fall through to error + } + if (!std::filesystem::exists(fullPath)) + throw Error("zone '%s' not found in checkout at '%s'", zonePath, fullPath.string()); + } auto storePath = StorePath::random(name); storeFS->mount(CanonPath(store->printStorePath(storePath)), makeDirtyAccessor()); diff --git a/src/libexpr/include/nix/expr/eval-settings.hh b/src/libexpr/include/nix/expr/eval-settings.hh index ff6ba75b2240..1cfeab849e57 100644 --- a/src/libexpr/include/nix/expr/eval-settings.hh +++ b/src/libexpr/include/nix/expr/eval-settings.hh @@ -438,6 +438,15 @@ struct EvalSettings : Config for tectonix builtins. This enables local development workflows where changes are visible before committing. )"}; + + Setting tectonixAutoSparseCheckout{ + this, + true, + "tectonix-auto-sparse-checkout", + R"( + When true, automatically add zones to the git sparse checkout + when they are referenced but not present on disk. + )"}; }; /** diff --git a/src/libexpr/include/nix/expr/eval.hh b/src/libexpr/include/nix/expr/eval.hh index 53d4b4982edc..1239afebe16e 100644 --- a/src/libexpr/include/nix/expr/eval.hh +++ b/src/libexpr/include/nix/expr/eval.hh @@ -565,6 +565,15 @@ private: */ mutable SharedSync> tectonixCheckoutZoneCache_; + /** Mutex for serializing sparse checkout modifications */ + mutable std::mutex sparseCheckoutMutex_; + + /** Resolve .git to actual git directory (handles worktrees). */ + std::filesystem::path resolveGitDir() const; + + /** Auto-add a zone to sparse checkout if not on disk. Returns true if added. */ + bool ensureZoneInSparseCheckout(std::string_view zonePath); + /** * Mount a zone by tree SHA, returning a (potentially virtual) store path. * Caches by tree SHA for deduplication across world revisions.