Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 99 additions & 22 deletions src/libexpr/eval.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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: <path>\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<std::string> & 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: <path>"
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: <path>\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
Expand All @@ -635,6 +638,69 @@ const std::set<std::string> & 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<std::mutex> 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<std::string>();
}
} 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<std::string, EvalState::ZoneDirtyInfo> & EvalState::getTectonixDirtyZones() const
{
std::call_once(tectonixDirtyZonesFlag, [this]() {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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());
Expand Down
9 changes: 9 additions & 0 deletions src/libexpr/include/nix/expr/eval-settings.hh
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,15 @@ struct EvalSettings : Config
for tectonix builtins. This enables local development workflows where changes
are visible before committing.
)"};

Setting<bool> 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.
)"};
};

/**
Expand Down
9 changes: 9 additions & 0 deletions src/libexpr/include/nix/expr/eval.hh
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,15 @@ private:
*/
mutable SharedSync<std::map<std::string, StorePath>> 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.
Expand Down
Loading