about summary refs log tree commit diff
path: root/third_party/nix/src/nix-channel/nix-channel.cc
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/nix/src/nix-channel/nix-channel.cc')
-rw-r--r--third_party/nix/src/nix-channel/nix-channel.cc275
1 files changed, 275 insertions, 0 deletions
diff --git a/third_party/nix/src/nix-channel/nix-channel.cc b/third_party/nix/src/nix-channel/nix-channel.cc
new file mode 100644
index 000000000000..5cc16d1b4b9a
--- /dev/null
+++ b/third_party/nix/src/nix-channel/nix-channel.cc
@@ -0,0 +1,275 @@
+#include <regex>
+
+#include <absl/strings/ascii.h>
+#include <absl/strings/str_split.h>
+#include <fcntl.h>
+#include <pwd.h>
+
+#include "libmain/shared.hh"
+#include "libstore/download.hh"
+#include "libstore/globals.hh"
+#include "libstore/store-api.hh"
+#include "nix/legacy.hh"
+
+using namespace nix;
+
+typedef std::map<std::string, std::string> Channels;
+
+static Channels channels;
+static Path channelsList;
+
+// Reads the list of channels.
+static void readChannels() {
+  if (!pathExists(channelsList)) {
+    return;
+  }
+  auto channelsFile = readFile(channelsList);
+
+  std::vector<std::string> lines =
+      absl::StrSplit(channelsFile, absl::ByChar('\n'), absl::SkipEmpty());
+
+  for (auto& line : lines) {
+    line = absl::StripTrailingAsciiWhitespace(line);
+    if (std::regex_search(line, std::regex("^\\s*\\#"))) {
+      continue;
+    }
+    std::vector<std::string> split =
+        absl::StrSplit(line, absl::ByChar(' '), absl::SkipEmpty());
+    auto url = std::regex_replace(split[0], std::regex("/*$"), "");
+    auto name = split.size() > 1 ? split[1] : baseNameOf(url);
+    channels[name] = url;
+  }
+}
+
+// Writes the list of channels.
+static void writeChannels() {
+  auto channelsFD = AutoCloseFD{open(
+      channelsList.c_str(), O_WRONLY | O_CLOEXEC | O_CREAT | O_TRUNC, 0644)};
+  if (!channelsFD) {
+    throw SysError(format("opening '%1%' for writing") % channelsList);
+  }
+  for (const auto& channel : channels) {
+    writeFull(channelsFD.get(), channel.second + " " + channel.first + "\n");
+  }
+}
+
+// Adds a channel.
+static void addChannel(const std::string& url, const std::string& name) {
+  if (!regex_search(url, std::regex("^(file|http|https)://"))) {
+    throw Error(format("invalid channel URL '%1%'") % url);
+  }
+  if (!regex_search(name, std::regex("^[a-zA-Z0-9_][a-zA-Z0-9_\\.-]*$"))) {
+    throw Error(format("invalid channel identifier '%1%'") % name);
+  }
+  readChannels();
+  channels[name] = url;
+  writeChannels();
+}
+
+static Path profile;
+
+// Remove a channel.
+static void removeChannel(const std::string& name) {
+  readChannels();
+  channels.erase(name);
+  writeChannels();
+
+  runProgram(settings.nixBinDir + "/nix-env", true,
+             {"--profile", profile, "--uninstall", name});
+}
+
+static Path nixDefExpr;
+
+// Fetch Nix expressions and binary cache URLs from the subscribed channels.
+static void update(const StringSet& channelNames) {
+  readChannels();
+
+  auto store = openStore();
+
+  // Download each channel.
+  Strings exprs;
+  for (const auto& channel : channels) {
+    auto name = channel.first;
+    auto url = channel.second;
+    if (!(channelNames.empty() || (channelNames.count(name) != 0u))) {
+      continue;
+    }
+
+    // We want to download the url to a file to see if it's a tarball while also
+    // checking if we got redirected in the process, so that we can grab the
+    // various parts of a nix channel definition from a consistent location if
+    // the redirect changes mid-download.
+    CachedDownloadRequest request(url);
+    request.ttl = 0;
+    auto dl = getDownloader();
+    auto result = dl->downloadCached(store, request);
+    auto filename = result.path;
+    url = absl::StripTrailingAsciiWhitespace(result.effectiveUri);
+
+    // If the URL contains a version number, append it to the name
+    // attribute (so that "nix-env -q" on the channels profile
+    // shows something useful).
+    auto cname = name;
+    std::smatch match;
+    auto urlBase = baseNameOf(url);
+    if (std::regex_search(urlBase, match, std::regex("(-\\d.*)$"))) {
+      cname = cname + std::string(match[1]);
+    }
+
+    std::string extraAttrs;
+
+    bool unpacked = false;
+    if (std::regex_search(filename, std::regex("\\.tar\\.(gz|bz2|xz)$"))) {
+      runProgram(settings.nixBinDir + "/nix-build", false,
+                 {"--no-out-link", "--expr",
+                  "import <nix/unpack-channel.nix> "
+                  "{ name = \"" +
+                      cname + "\"; channelName = \"" + name +
+                      "\"; src = builtins.storePath \"" + filename + "\"; }"});
+      unpacked = true;
+    }
+
+    if (!unpacked) {
+      // Download the channel tarball.
+      try {
+        filename = dl->downloadCached(
+                         store, CachedDownloadRequest(url + "/nixexprs.tar.xz"))
+                       .path;
+      } catch (DownloadError& e) {
+        filename =
+            dl->downloadCached(store,
+                               CachedDownloadRequest(url + "/nixexprs.tar.bz2"))
+                .path;
+      }
+      filename = absl::StripTrailingAsciiWhitespace(filename);
+    }
+
+    // Regardless of where it came from, add the expression representing this
+    // channel to accumulated expression
+    exprs.push_back("f: f { name = \"" + cname + "\"; channelName = \"" + name +
+                    "\"; src = builtins.storePath \"" + filename + "\"; " +
+                    extraAttrs + " }");
+  }
+
+  // Unpack the channel tarballs into the Nix store and install them
+  // into the channels profile.
+  std::cerr << "unpacking channels...\n";
+  Strings envArgs{"--profile", profile,
+                  "--file",    "<nix/unpack-channel.nix>",
+                  "--install", "--from-expression"};
+  for (auto& expr : exprs) {
+    envArgs.push_back(std::move(expr));
+  }
+  envArgs.push_back("--quiet");
+  runProgram(settings.nixBinDir + "/nix-env", false, envArgs);
+
+  // Make the channels appear in nix-env.
+  struct stat st;
+  if (lstat(nixDefExpr.c_str(), &st) == 0) {
+    if (S_ISLNK(st.st_mode)) {
+      // old-skool ~/.nix-defexpr
+      if (unlink(nixDefExpr.c_str()) == -1) {
+        throw SysError(format("unlinking %1%") % nixDefExpr);
+      }
+    }
+  } else if (errno != ENOENT) {
+    throw SysError(format("getting status of %1%") % nixDefExpr);
+  }
+  createDirs(nixDefExpr);
+  auto channelLink = nixDefExpr + "/channels";
+  replaceSymlink(profile, channelLink);
+}
+
+static int _main(int argc, char** argv) {
+  {
+    // Figure out the name of the `.nix-channels' file to use
+    auto home = getHome();
+    channelsList = home + "/.nix-channels";
+    nixDefExpr = home + "/.nix-defexpr";
+
+    // Figure out the name of the channels profile.
+    profile = fmt("%s/profiles/per-user/%s/channels", settings.nixStateDir,
+                  getUserName());
+
+    enum { cNone, cAdd, cRemove, cList, cUpdate, cRollback } cmd = cNone;
+    std::vector<std::string> args;
+    parseCmdLine(argc, argv,
+                 [&](Strings::iterator& arg, const Strings::iterator& end) {
+                   if (*arg == "--help") {
+                     showManPage("nix-channel");
+                   } else if (*arg == "--version") {
+                     printVersion("nix-channel");
+                   } else if (*arg == "--add") {
+                     cmd = cAdd;
+                   } else if (*arg == "--remove") {
+                     cmd = cRemove;
+                   } else if (*arg == "--list") {
+                     cmd = cList;
+                   } else if (*arg == "--update") {
+                     cmd = cUpdate;
+                   } else if (*arg == "--rollback") {
+                     cmd = cRollback;
+                   } else {
+                     args.push_back(std::move(*arg));
+                   }
+                   return true;
+                 });
+
+    switch (cmd) {
+      case cNone:
+        throw UsageError("no command specified");
+      case cAdd:
+        if (args.empty() || args.size() > 2) {
+          throw UsageError("'--add' requires one or two arguments");
+        }
+        {
+          auto url = args[0];
+          std::string name;
+          if (args.size() == 2) {
+            name = args[1];
+          } else {
+            name = baseNameOf(url);
+            name = std::regex_replace(name, std::regex("-unstable$"), "");
+            name = std::regex_replace(name, std::regex("-stable$"), "");
+          }
+          addChannel(url, name);
+        }
+        break;
+      case cRemove:
+        if (args.size() != 1) {
+          throw UsageError("'--remove' requires one argument");
+        }
+        removeChannel(args[0]);
+        break;
+      case cList:
+        if (!args.empty()) {
+          throw UsageError("'--list' expects no arguments");
+        }
+        readChannels();
+        for (const auto& channel : channels) {
+          std::cout << channel.first << ' ' << channel.second << '\n';
+        }
+        break;
+      case cUpdate:
+        update(StringSet(args.begin(), args.end()));
+        break;
+      case cRollback:
+        if (args.size() > 1) {
+          throw UsageError("'--rollback' has at most one argument");
+        }
+        Strings envArgs{"--profile", profile};
+        if (args.size() == 1) {
+          envArgs.push_back("--switch-generation");
+          envArgs.push_back(args[0]);
+        } else {
+          envArgs.push_back("--rollback");
+        }
+        runProgram(settings.nixBinDir + "/nix-env", false, envArgs);
+        break;
+    }
+
+    return 0;
+  }
+}
+
+static RegisterLegacyCommand s1("nix-channel", _main);