#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')); 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(' ')); 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);