From 4b313ceb9ea94d5b6e37c70f3e6130db0eedab0a Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Wed, 23 Feb 2022 14:39:07 +0100 Subject: [PATCH] fetchTree: Support applying patches You can now write fetchTree { type = "github"; owner = "NixOS"; repo = "nixpkgs"; rev = "0f316e4d72daed659233817ffe52bf08e081b5de"; patches = [ ./thunderbird-1.patch ./thunderbird-2.patch ]; }; to apply a list of patches to a tree. These are applied lazily - the patched tree is not materialized unless you do something that causes the entire tree to be copied to the store (like 'src = fetchTree { ... }'). The equivalent of '-p1' is implied. File additions/deletions/renames are not yet handled. Issue #3920. --- src/libexpr/primops/fetchTree.cc | 19 ++++ src/libfetchers/input-accessor.hh | 4 + src/libfetchers/patching-input-accessor.cc | 115 +++++++++++++++++++++ src/libutil/tests/tests.cc | 36 +++++++ src/libutil/util.cc | 15 +++ src/libutil/util.hh | 6 ++ 6 files changed, 195 insertions(+) create mode 100644 src/libfetchers/patching-input-accessor.cc diff --git a/src/libexpr/primops/fetchTree.cc b/src/libexpr/primops/fetchTree.cc index 01691ef6b..b9eabea3e 100644 --- a/src/libexpr/primops/fetchTree.cc +++ b/src/libexpr/primops/fetchTree.cc @@ -105,6 +105,7 @@ static void fetchTree( ) { fetchers::Input input; PathSet context; + std::vector patches; state.forceValue(*args[0], pos); @@ -130,7 +131,22 @@ static void fetchTree( for (auto & attr : *args[0]->attrs) { if (attr.name == state.sType) continue; + + if (attr.name == "patches") { + state.forceList(*attr.value, *attr.pos); + + for (auto elem : attr.value->listItems()) { + // FIXME: use realisePath + PathSet context; + auto patchFile = state.unpackPath(state.coerceToPath(pos, *elem, context)); + patches.push_back(patchFile.accessor->readFile(patchFile.path)); + } + + continue; + } + state.forceValue(*attr.value, *attr.pos); + if (attr.value->type() == nPath || attr.value->type() == nString) { auto s = state.coerceToString(*attr.pos, *attr.value, context, false, false).toOwned(); attrs.emplace(attr.name, @@ -178,6 +194,9 @@ static void fetchTree( auto [accessor, input2] = input.lazyFetch(state.store); + if (!patches.empty()) + accessor = makePatchingInputAccessor(accessor, patches); + //state.allowPath(tree.storePath); emitTreeAttrs( diff --git a/src/libfetchers/input-accessor.hh b/src/libfetchers/input-accessor.hh index 7a4dd08a6..872fcd3cd 100644 --- a/src/libfetchers/input-accessor.hh +++ b/src/libfetchers/input-accessor.hh @@ -57,6 +57,10 @@ ref makeMemoryInputAccessor(); ref makeZipInputAccessor(PathView path); +ref makePatchingInputAccessor( + ref next, + const std::vector & patches); + struct SourcePath { ref accessor; diff --git a/src/libfetchers/patching-input-accessor.cc b/src/libfetchers/patching-input-accessor.cc new file mode 100644 index 000000000..269f13c22 --- /dev/null +++ b/src/libfetchers/patching-input-accessor.cc @@ -0,0 +1,115 @@ +#include "input-accessor.hh" + +namespace nix { + +// TODO: handle file creation / deletion. +struct PatchingInputAccessor : InputAccessor +{ + ref next; + + std::map> patchesPerFile; + + PatchingInputAccessor( + ref next, + const std::vector & patches) + : next(next) + { + /* Extract the patches for each file. */ + for (auto & patch : patches) { + std::string_view p = patch; + std::string_view start; + std::string_view fileName; + + auto flush = [&]() + { + if (start.empty()) return; + auto contents = start.substr(0, p.data() - start.data()); + start = ""; + auto slash = fileName.find('/'); + if (slash == fileName.npos) return; + fileName = fileName.substr(slash); + debug("found patch for '%s'", fileName); + patchesPerFile.emplace(Path(fileName), std::vector()) + .first->second.push_back(std::string(contents)); + }; + + while (!p.empty()) { + auto [line, rest] = getLine(p); + + if (hasPrefix(line, "--- ")) { + flush(); + start = p; + fileName = line.substr(4); + } + + if (!start.empty()) { + if (!(hasPrefix(line, "+++ ") + || hasPrefix(line, "@@") + || hasPrefix(line, "+") + || hasPrefix(line, "-") + || hasPrefix(line, " "))) + { + flush(); + } + } + + p = rest; + } + + flush(); + } + } + + std::string readFile(PathView path) override + { + auto contents = next->readFile(path); + + auto i = patchesPerFile.find((Path) path); + if (i != patchesPerFile.end()) { + for (auto & patch : i->second) { + auto tempDir = createTempDir(); + AutoDelete del(tempDir); + auto sourceFile = tempDir + "/source"; + auto rejFile = tempDir + "/source.rej"; + writeFile(sourceFile, contents); + try { + contents = runProgram("patch", true, {"--quiet", sourceFile, "--output=-", "-r", rejFile}, patch); + } catch (ExecError & e) { + del.cancel(); + throw; + } + } + } + + return contents; + } + + bool pathExists(PathView path) override + { + return next->pathExists(path); + } + + Stat lstat(PathView path) override + { + return next->lstat(path); + } + + DirEntries readDirectory(PathView path) override + { + return next->readDirectory(path); + } + + std::string readLink(PathView path) override + { + return next->readLink(path); + } +}; + +ref makePatchingInputAccessor( + ref next, + const std::vector & patches) +{ + return make_ref(next, std::move(patches)); +} + +} diff --git a/src/libutil/tests/tests.cc b/src/libutil/tests/tests.cc index 92972ed14..a74110dfe 100644 --- a/src/libutil/tests/tests.cc +++ b/src/libutil/tests/tests.cc @@ -311,6 +311,42 @@ namespace nix { ASSERT_THROW(base64Decode("cXVvZCBlcm_0IGRlbW9uc3RyYW5kdW0="), Error); } + /* ---------------------------------------------------------------------------- + * getLine + * --------------------------------------------------------------------------*/ + + TEST(getLine, all) { + { + auto [line, rest] = getLine("foo\nbar\nxyzzy"); + ASSERT_EQ(line, "foo"); + ASSERT_EQ(rest, "bar\nxyzzy"); + } + + { + auto [line, rest] = getLine("foo\r\nbar\r\nxyzzy"); + ASSERT_EQ(line, "foo"); + ASSERT_EQ(rest, "bar\r\nxyzzy"); + } + + { + auto [line, rest] = getLine("foo\n"); + ASSERT_EQ(line, "foo"); + ASSERT_EQ(rest, ""); + } + + { + auto [line, rest] = getLine("foo"); + ASSERT_EQ(line, "foo"); + ASSERT_EQ(rest, ""); + } + + { + auto [line, rest] = getLine(""); + ASSERT_EQ(line, ""); + ASSERT_EQ(rest, ""); + } + } + /* ---------------------------------------------------------------------------- * toLower * --------------------------------------------------------------------------*/ diff --git a/src/libutil/util.cc b/src/libutil/util.cc index 70eaf4f9c..5a3a91fa8 100644 --- a/src/libutil/util.cc +++ b/src/libutil/util.cc @@ -1564,6 +1564,21 @@ std::string stripIndentation(std::string_view s) } +std::pair getLine(std::string_view s) +{ + auto newline = s.find('\n'); + + if (newline == s.npos) { + return {s, ""}; + } else { + auto line = s.substr(0, newline); + if (!line.empty() && line[line.size() - 1] == '\r') + line = line.substr(0, line.size() - 1); + return {line, s.substr(newline + 1)}; + } +} + + ////////////////////////////////////////////////////////////////////// diff --git a/src/libutil/util.hh b/src/libutil/util.hh index 20591952d..f87a7447d 100644 --- a/src/libutil/util.hh +++ b/src/libutil/util.hh @@ -537,6 +537,12 @@ std::string base64Decode(std::string_view s); std::string stripIndentation(std::string_view s); +/* Get the prefix of 's' up to and excluding the next line break (LF + optionally preceded by CR), and the remainder following the line + break. */ +std::pair getLine(std::string_view s); + + /* Get a value for the specified key from an associate container. */ template std::optional get(const T & map, const typename T::key_type & key)