1
1
Fork 0
mirror of https://github.com/NixOS/nix.git synced 2025-11-23 10:49:36 +01:00

Merge remote-tracking branch 'upstream/2.3-maintenance' into builder-host-key-stable

This commit is contained in:
John Ericson 2023-10-26 16:01:18 -04:00
commit b4abe56a23
54 changed files with 776 additions and 402 deletions

View file

@ -9,7 +9,7 @@ libexpr_SOURCES := $(wildcard $(d)/*.cc) $(wildcard $(d)/primops/*.cc) $(d)/lexe
libexpr_LIBS = libutil libstore
libexpr_LDFLAGS =
ifneq ($(OS), FreeBSD)
ifdef HOST_LINUX
libexpr_LDFLAGS += -ldl
endif

View file

@ -105,7 +105,7 @@ void ExprAttrs::show(std::ostream & str) const
str << "{ ";
for (auto & i : attrs)
if (i.second.inherited)
str << "inherit " << i.first << " " << "; ";
str << "inherit " << i.first << "; ";
else
str << i.first << " = " << *i.second.e << "; ";
for (auto & i : dynamicAttrs)
@ -211,7 +211,7 @@ string showAttrPath(const AttrPath & attrPath)
if (i.symbol.set())
out << i.symbol;
else
out << "\"${" << *i.expr << "}\"";
out << "${" << *i.expr << "}";
}
return out.str();
}

View file

@ -937,10 +937,16 @@ static void prim_hashFile(EvalState & state, const Pos & pos, Value * * args, Va
if (ht == htUnknown)
throw Error(format("unknown hash type '%1%', at %2%") % type % pos);
PathSet context; // discarded
Path p = state.coerceToPath(pos, *args[1], context);
PathSet context;
Path path = state.coerceToPath(pos, *args[1], context);
try {
state.realiseContext(context);
} catch (InvalidPathError & e) {
throw EvalError(format("cannot read '%1%', since path '%2%' is not valid, at %3%")
% path % e.path % pos);
}
mkString(v, hashFile(ht, state.checkSourcePath(p)).to_string(Base16, false), context);
mkString(v, hashFile(ht, state.checkSourcePath(state.toRealPath(path, context))).to_string(Base16, false));
}
/* Read a directory (without . or ..) */
@ -1350,6 +1356,10 @@ static void prim_catAttrs(EvalState & state, const Pos & pos, Value * * args, Va
static void prim_functionArgs(EvalState & state, const Pos & pos, Value * * args, Value & v)
{
state.forceValue(*args[0]);
if (args[0]->type == tPrimOpApp || args[0]->type == tPrimOp) {
state.mkAttrs(v, 0);
return;
}
if (args[0]->type != tLambda)
throw TypeError(format("'functionArgs' requires a function, at %1%") % pos);
@ -1817,7 +1827,7 @@ static void prim_hashString(EvalState & state, const Pos & pos, Value * * args,
PathSet context; // discarded
string s = state.forceString(*args[1], context, pos);
mkString(v, hashString(ht, s).to_string(Base16, false), context);
mkString(v, hashString(ht, s).to_string(Base16, false));
}
@ -2075,7 +2085,7 @@ void fetch(EvalState & state, const Pos & pos, Value * * args, Value & v,
else if (n == "name")
request.name = state.forceStringNoCtx(*attr.value, *attr.pos);
else
throw EvalError(format("unsupported argument '%1%' to '%2%', at %3%") % attr.name % who % attr.pos);
throw EvalError(format("unsupported argument '%1%' to '%2%', at %3%") % attr.name % who % *attr.pos);
}
if (request.uri.empty())

View file

@ -6,6 +6,7 @@
#include "hash.hh"
#include <sys/time.h>
#include <sys/wait.h>
#include <regex>
@ -173,7 +174,7 @@ GitInfo exportGit(ref<Store> store, const std::string & uri,
Path tmpDir = createTempDir();
AutoDelete delTmpDir(tmpDir, true);
runProgram("tar", true, { "x", "-C", tmpDir }, tar);
runProgram("tar", true, { "-x", "-f", "-", "-C", tmpDir }, tar);
gitInfo.storePath = store->addToStore(name, tmpDir);

View file

@ -14,6 +14,14 @@
#include <sys/stat.h>
#include <unistd.h>
#include <signal.h>
#ifdef __linux__
#include <features.h>
#endif
#ifdef __GLIBC__
#include <gnu/lib-names.h>
#include <nss.h>
#include <dlfcn.h>
#endif
#include <openssl/crypto.h>
@ -95,6 +103,40 @@ static void opensslLockCallback(int mode, int type, const char * file, int line)
}
#endif
static std::once_flag dns_resolve_flag;
static void preloadNSS() {
/* builtin:fetchurl can trigger a DNS lookup, which with glibc can trigger a dynamic library load of
one of the glibc NSS libraries in a sandboxed child, which will fail unless the library's already
been loaded in the parent. So we force a lookup of an invalid domain to force the NSS machinery to
load its lookup libraries in the parent before any child gets a chance to. */
std::call_once(dns_resolve_flag, []() {
#ifdef __GLIBC__
/* On linux, glibc will run every lookup through the nss layer.
* That means every lookup goes, by default, through nscd, which acts as a local
* cache.
* Because we run builds in a sandbox, we also remove access to nscd otherwise
* lookups would leak into the sandbox.
*
* But now we have a new problem, we need to make sure the nss_dns backend that
* does the dns lookups when nscd is not available is loaded or available.
*
* We can't make it available without leaking nix's environment, so instead we'll
* load the backend, and configure nss so it does not try to run dns lookups
* through nscd.
*
* This is technically only used for builtins:fetch* functions so we only care
* about dns.
*
* All other platforms are unaffected.
*/
if (dlopen (LIBNSS_DNS_SO, RTLD_NOW) == NULL) {
printMsg(Verbosity::lvlWarn, fmt("Unable to load nss_dns backend"));
}
__nss_configure_lookup ("hosts", "dns");
#endif
});
}
static void sigHandler(int signo) { }
@ -158,6 +200,8 @@ void initNix()
if (hasPrefix(getEnv("TMPDIR"), "/var/folders/"))
unsetenv("TMPDIR");
#endif
preloadNSS();
}

View file

@ -34,7 +34,6 @@
#include <sys/resource.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <netdb.h>
#include <unistd.h>
#include <errno.h>
#include <cstring>
@ -45,7 +44,6 @@
/* Includes required for chroot support. */
#if __linux__
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <netinet/ip.h>
@ -181,6 +179,8 @@ public:
virtual string key() = 0;
virtual void cleanup() { }
protected:
virtual void amDone(ExitCode result);
@ -426,6 +426,8 @@ void Goal::amDone(ExitCode result)
}
waiters.clear();
worker.removeGoal(shared_from_this());
cleanup();
}
@ -1222,8 +1224,13 @@ void DerivationGoal::outputsSubstituted()
/* If the substitutes form an incomplete closure, then we should
build the dependencies of this derivation, but after that, we
can still use the substitutes for this derivation itself. */
if (nrIncompleteClosure > 0) retrySubstitution = true;
can still use the substitutes for this derivation itself.
If the nrIncompleteClosure != nrFailed, we have another issue as well.
In particular, it may be the case that the hole in the closure is
an output of the current derivation, which causes a loop if retried.
*/
if (nrIncompleteClosure > 0 && nrIncompleteClosure == nrFailed) retrySubstitution = true;
nrFailed = nrNoSubstituters = nrIncompleteClosure = 0;
@ -1881,22 +1888,6 @@ PathSet DerivationGoal::exportReferences(PathSet storePaths)
return paths;
}
static std::once_flag dns_resolve_flag;
static void preloadNSS() {
/* builtin:fetchurl can trigger a DNS lookup, which with glibc can trigger a dynamic library load of
one of the glibc NSS libraries in a sandboxed child, which will fail unless the library's already
been loaded in the parent. So we force a lookup of an invalid domain to force the NSS machinery to
load its lookup libraries in the parent before any child gets a chance to. */
std::call_once(dns_resolve_flag, []() {
struct addrinfo *res = NULL;
if (getaddrinfo("this.pre-initializes.the.dns.resolvers.invalid.", "http", NULL, &res) != 0) {
if (res) freeaddrinfo(res);
}
});
}
void DerivationGoal::startBuilder()
{
/* Right platform? */
@ -1908,9 +1899,6 @@ void DerivationGoal::startBuilder()
settings.thisSystem,
concatStringsSep(", ", settings.systemFeatures));
if (drv->isBuiltin())
preloadNSS();
#if __APPLE__
additionalSandboxProfile = parsedDrv->getStringAttr("__sandboxProfile").value_or("");
#endif
@ -2060,7 +2048,9 @@ void DerivationGoal::startBuilder()
if (!found)
throw Error(format("derivation '%1%' requested impure path '%2%', but it was not in allowed-impure-host-deps") % drvPath % i);
dirsInChroot[i] = i;
/* Allow files in __impureHostDeps to be missing; e.g.
macOS 11+ has no /usr/lib/libSystem*.dylib */
dirsInChroot[i] = {i, true};
}
#if __linux__
@ -2834,8 +2824,6 @@ void DerivationGoal::runChild()
ss.push_back("/etc/services");
ss.push_back("/etc/hosts");
if (pathExists("/var/run/nscd/socket"))
ss.push_back("/var/run/nscd/socket");
}
for (auto & i : ss) dirsInChroot.emplace(i, i);
@ -3911,6 +3899,8 @@ public:
void handleChildOutput(int fd, const string & data) override;
void handleEOF(int fd) override;
void cleanup() override;
Path getStorePath() { return storePath; }
void amDone(ExitCode result) override
@ -3934,15 +3924,7 @@ SubstitutionGoal::SubstitutionGoal(const Path & storePath, Worker & worker, Repa
SubstitutionGoal::~SubstitutionGoal()
{
try {
if (thr.joinable()) {
// FIXME: signal worker thread to quit.
thr.join();
worker.childTerminated(this);
}
} catch (...) {
ignoreException();
}
cleanup();
}
@ -3977,6 +3959,8 @@ void SubstitutionGoal::tryNext()
{
trace("trying next substituter");
cleanup();
if (subs.size() == 0) {
/* None left. Terminate this goal and let someone else deal
with it. */
@ -4104,7 +4088,7 @@ void SubstitutionGoal::tryToRun()
thr = std::thread([this]() {
try {
/* Wake up the worker loop when we're done. */
Finally updateStats([this]() { outPipe.writeSide = -1; });
Finally updateStats([this]() { outPipe.writeSide.close(); });
Activity act(*logger, actSubstitute, Logger::Fields{storePath, sub->getUri()});
PushActivity pact(act.id);
@ -4188,6 +4172,20 @@ void SubstitutionGoal::handleEOF(int fd)
if (fd == outPipe.readSide.get()) worker.wakeUp(shared_from_this());
}
void SubstitutionGoal::cleanup()
{
try {
if (thr.joinable()) {
// FIXME: signal worker thread to quit.
thr.join();
worker.childTerminated(this);
}
outPipe.close();
} catch (...) {
ignoreException();
}
}
//////////////////////////////////////////////////////////////////////

View file

@ -349,6 +349,13 @@ struct CurlDownloader : public Downloader
(httpStatus == 200 || httpStatus == 201 || httpStatus == 204 || httpStatus == 206 || httpStatus == 304 || httpStatus == 226 /* FTP */ || httpStatus == 0 /* other protocol */))
{
result.cached = httpStatus == 304;
// In 2021, GitHub responds to If-None-Match with 304,
// but omits ETag. We just use the If-None-Match etag
// since 304 implies they are the same.
if (httpStatus == 304 && result.etag == "")
result.etag = request.expectedETag;
act.progress(result.bodySize, result.bodySize);
done = true;
callback(std::move(result));

View file

@ -34,7 +34,7 @@ Settings::Settings()
, nixLibexecDir(canonPath(getEnv("NIX_LIBEXEC_DIR", NIX_LIBEXEC_DIR)))
, nixBinDir(canonPath(getEnv("NIX_BIN_DIR", NIX_BIN_DIR)))
, nixManDir(canonPath(NIX_MAN_DIR))
, nixDaemonSocketFile(canonPath(nixStateDir + DEFAULT_SOCKET_PATH))
, nixDaemonSocketFile(canonPath(getEnv("NIX_DAEMON_SOCKET_PATH", nixStateDir + DEFAULT_SOCKET_PATH)))
{
buildUsersGroup = getuid() == 0 ? "nixbld" : "";
lockCPU = getEnv("NIX_AFFINITY_HACK", "1") == "1";

View file

@ -1029,6 +1029,40 @@ void LocalStore::addToStore(const ValidPathInfo & info, Source & source,
throw Error("size mismatch importing path '%s';\n wanted: %s\n got: %s",
info.path, info.narSize, hashResult.second);
if (!info.ca.empty()) {
auto ca = info.ca;
if (hasPrefix(ca, "fixed:")) {
bool recursive = ca.compare(6, 2, "r:") == 0;
Hash expectedHash(std::string(ca, recursive ? 8 : 6));
if (info.references.empty()) {
auto actualFoHash = hashCAPath(
recursive,
expectedHash.type,
realPath
);
if (ca != actualFoHash) {
throw Error("ca hash mismatch importing path '%s';\n specified: %s\n got: %s",
info.path,
ca,
actualFoHash);
}
} else {
throw Error("path '%s' claims to be content-addressed, but has references. This isnt allowed",
info.path);
}
} else if (hasPrefix(ca, "text:")) {
Hash textHash(std::string(ca, 5));
auto actualTextHash = hashString(htSHA256, readFile(realPath));
if (textHash != actualTextHash) {
throw Error("ca hash mismatch importing path '%s';\n specified: %s\n got: %s",
info.path,
textHash.to_string(Base32, true),
actualTextHash.to_string(Base32, true));
}
}
}
autoGC();
canonicalisePathMetaData(realPath, -1);
@ -1450,4 +1484,20 @@ void LocalStore::createUser(const std::string & userName, uid_t userId)
}
std::string LocalStore::hashCAPath(
bool recursive,
const HashType & hashType,
const Path & path
)
{
HashSink caSink(hashType);
if (recursive) {
dumpPath(path, caSink);
} else {
readFile(path, caSink);
}
auto hash = caSink.finish().first;
return makeFixedOutputCA(recursive, hash);
}
}

View file

@ -295,8 +295,14 @@ private:
void createUser(const std::string & userName, uid_t userId) override;
friend class DerivationGoal;
friend class SubstitutionGoal;
std::string hashCAPath(
bool recursive,
const HashType & hashType,
const Path & path
);
friend struct DerivationGoal;
friend struct SubstitutionGoal;
};

View file

@ -9,7 +9,7 @@ libstore_SOURCES := $(wildcard $(d)/*.cc $(d)/builtins/*.cc)
libstore_LIBS = libutil
libstore_LDFLAGS = $(SQLITE3_LIBS) -lbz2 $(LIBCURL_LIBS) $(SODIUM_LIBS) -pthread
ifneq ($(OS), FreeBSD)
ifdef HOST_LINUX
libstore_LDFLAGS += -ldl
endif
@ -21,7 +21,7 @@ ifeq ($(ENABLE_S3), 1)
libstore_LDFLAGS += -laws-cpp-sdk-transfer -laws-cpp-sdk-s3 -laws-cpp-sdk-core
endif
ifeq ($(OS), SunOS)
ifdef HOST_SOLARIS
libstore_LDFLAGS += -lsocket
endif

View file

@ -56,6 +56,10 @@ class AwsLogger : public Aws::Utils::Logging::FormattedLogSystem
{
debug("AWS: %s", chomp(statement));
}
#if !(AWS_VERSION_MAJOR <= 1 && AWS_VERSION_MINOR <= 7 && AWS_VERSION_PATCH <= 115)
void Flush() override {}
#endif
};
static void initAWS()

View file

@ -32,7 +32,9 @@
(literal "/tmp") (subpath TMPDIR))
; Some packages like to read the system version.
(allow file-read* (literal "/System/Library/CoreServices/SystemVersion.plist"))
(allow file-read*
(literal "/System/Library/CoreServices/SystemVersion.plist")
(literal "/System/Library/CoreServices/SystemVersionCompat.plist"))
; Without this line clang cannot write to /dev/null, breaking some configure tests.
(allow file-read-metadata (literal "/dev"))

View file

@ -1,4 +1,5 @@
#include "sqlite.hh"
#include "globals.hh"
#include "util.hh"
#include <sqlite3.h>
@ -27,8 +28,12 @@ namespace nix {
SQLite::SQLite(const Path & path)
{
// useSQLiteWAL also indicates what virtual file system we need. Using
// `unix-dotfile` is needed on NFS file systems and on Windows' Subsystem
// for Linux (WSL) where useSQLiteWAL should be false by default.
const char *vfs = settings.useSQLiteWAL ? 0 : "unix-dotfile";
if (sqlite3_open_v2(path.c_str(), &db,
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, 0) != SQLITE_OK)
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, vfs) != SQLITE_OK)
throw Error(format("cannot open SQLite database '%s'") % path);
}

View file

@ -5,8 +5,10 @@
#include <lzma.h>
#include <bzlib.h>
#include <zstd.h>
#include <cstdio>
#include <cstring>
#include <vector>
#include <brotli/decode.h>
#include <brotli/encode.h>
@ -198,6 +200,78 @@ struct BrotliDecompressionSink : ChunkedCompressionSink
}
};
struct ZstdDecompressionSink : CompressionSink
{
Sink & nextSink;
ZSTD_DStream *strm;
std::vector<uint8_t> inbuf;
size_t outbuf_size = ZSTD_DStreamOutSize();
uint8_t *outbuf = new uint8_t[outbuf_size];
ZstdDecompressionSink(Sink & nextSink) : nextSink(nextSink)
{
strm = ZSTD_createDStream();
if (!strm)
throw CompressionError("unable to initialise zstd decoder");
ZSTD_initDStream(strm);
}
~ZstdDecompressionSink()
{
delete[] outbuf;
ZSTD_freeDStream(strm);
}
void finish() override
{
// this call doesn't make any sense, but it's here for consistency with the other compression sinks
// CompressionSink inherits from BufferedSink, but none of the subclasses appear to ever make use of the buffer
flush();
// if we still have undecoded data in the input buffer, we can't signal EOF to libzstd
// if we don't, then we're done here anyway
if (inbuf.size())
throw CompressionError("received unexpected EOF while decompressing zstd file");
nextSink(nullptr, 0);
}
void write(const unsigned char * data, size_t len) override
{
inbuf.insert(inbuf.end(), data, data + len);
ZSTD_inBuffer in = {
.src = inbuf.data(),
.size = inbuf.size(),
.pos = 0
};
ZSTD_outBuffer out = {
.dst = outbuf,
.size = outbuf_size,
.pos = 0
};
while (in.pos < in.size) {
out.pos = 0;
size_t ret = ZSTD_decompressStream(strm, &out, &in);
if (ZSTD_isError(ret))
throw CompressionError("error %s while decompressing zstd file", ZSTD_getErrorName(ret));
if (out.pos)
nextSink(outbuf, out.pos);
else
break;
}
// drop consumed input
inbuf.erase(inbuf.begin(), inbuf.begin() + in.pos);
}
};
ref<std::string> decompress(const std::string & method, const std::string & in)
{
StringSink ssink;
@ -217,6 +291,8 @@ ref<CompressionSink> makeDecompressionSink(const std::string & method, Sink & ne
return make_ref<BzipDecompressionSink>(nextSink);
else if (method == "br")
return make_ref<BrotliDecompressionSink>(nextSink);
else if (method == "zstd")
return make_ref<ZstdDecompressionSink>(nextSink);
else
throw UnknownCompressionMethod("unknown compression method '%s'", method);
}

View file

@ -1,6 +1,7 @@
#include "json.hh"
#include <iomanip>
#include <cstdint>
#include <cstring>
namespace nix {

View file

@ -6,4 +6,4 @@ libutil_DIR := $(d)
libutil_SOURCES := $(wildcard $(d)/*.cc)
libutil_LDFLAGS = $(LIBLZMA_LIBS) -lbz2 -pthread $(OPENSSL_LIBS) $(LIBBROTLI_LIBS) $(BOOST_LDFLAGS) -lboost_context
libutil_LDFLAGS = $(LIBLZMA_LIBS) -lbz2 -pthread $(OPENSSL_LIBS) $(LIBBROTLI_LIBS) $(LIBZSTD_LIBS) $(BOOST_LDFLAGS) -lboost_context

View file

@ -753,6 +753,7 @@ void AutoCloseFD::close()
if (::close(fd) == -1)
/* This should never happen. */
throw SysError(format("closing file descriptor %1%") % fd);
fd = -1;
}
}
@ -770,6 +771,12 @@ int AutoCloseFD::release()
return oldFD;
}
void Pipe::close()
{
readSide.close();
writeSide.close();
}
void Pipe::create()
{
@ -1080,7 +1087,7 @@ void runProgram2(const RunOptions & options)
throw SysError("executing '%1%'", options.program);
}, processOptions);
out.writeSide = -1;
out.writeSide.close();
std::thread writerThread;
@ -1093,7 +1100,7 @@ void runProgram2(const RunOptions & options)
if (source) {
in.readSide = -1;
in.readSide.close();
writerThread = std::thread([&]() {
try {
std::vector<unsigned char> buf(8 * 1024);
@ -1110,7 +1117,7 @@ void runProgram2(const RunOptions & options)
} catch (...) {
promise.set_exception(std::current_exception());
}
in.writeSide = -1;
in.writeSide.close();
});
}

View file

@ -190,7 +190,6 @@ public:
class AutoCloseFD
{
int fd;
void close();
public:
AutoCloseFD();
AutoCloseFD(int fd);
@ -202,6 +201,7 @@ public:
int get() const;
explicit operator bool() const;
int release();
void close();
};
@ -210,6 +210,7 @@ class Pipe
public:
AutoCloseFD readSide, writeSide;
void create();
void close();
};

View file

@ -106,7 +106,7 @@ static void _main(int argc, char * * argv)
// Heuristic to see if we're invoked as a shebang script, namely,
// if we have at least one argument, it's the name of an
// executable file, and it starts with "#!".
if (runEnv && argc > 1 && !std::regex_search(argv[1], std::regex("nix-shell"))) {
if (runEnv && argc > 1) {
script = argv[1];
try {
auto lines = tokenizeString<Strings>(readFile(script), "\n");
@ -425,6 +425,7 @@ static void _main(int argc, char * * argv)
"unset NIX_ENFORCE_PURITY; "
"shopt -u nullglob; "
"unset TZ; %6%"
"shopt -s execfail;"
"%7%",
(Path) tmpDir,
(pure ? "" : "p=$PATH; "),

View file

@ -901,7 +901,11 @@ static PeerInfo getPeerInfo(int remote)
#if defined(SO_PEERCRED)
#if defined(__OpenBSD__)
struct sockpeercred cred;
#else
ucred cred;
#endif
socklen_t credLen = sizeof(cred);
if (getsockopt(remote, SOL_SOCKET, SO_PEERCRED, &cred, &credLen) == -1)
throw SysError("getting peer credentials");

View file

@ -50,10 +50,12 @@ bool createUserEnv(EvalState & state, DrvInfos & elems,
output paths, and optionally the derivation path, as well
as the meta attributes. */
Path drvPath = keepDerivations ? i.queryDrvPath() : "";
DrvInfo::Outputs outputs = i.queryOutputs(true);
StringSet metaNames = i.queryMetaNames();
Value & v(*state.allocValue());
manifest.listElems()[n++] = &v;
state.mkAttrs(v, 16);
state.mkAttrs(v, 7 + outputs.size());
mkString(*state.allocAttr(v, state.sType), "derivation");
mkString(*state.allocAttr(v, state.sName), i.queryName());
@ -65,7 +67,6 @@ bool createUserEnv(EvalState & state, DrvInfos & elems,
mkString(*state.allocAttr(v, state.sDrvPath), i.queryDrvPath());
// Copy each output meant for installation.
DrvInfo::Outputs outputs = i.queryOutputs(true);
Value & vOutputs = *state.allocAttr(v, state.sOutputs);
state.mkList(vOutputs, outputs.size());
unsigned int m = 0;
@ -85,8 +86,7 @@ bool createUserEnv(EvalState & state, DrvInfos & elems,
// Copy the meta attributes.
Value & vMeta = *state.allocAttr(v, state.sMeta);
state.mkAttrs(vMeta, 16);
StringSet metaNames = i.queryMetaNames();
state.mkAttrs(vMeta, metaNames.size());
for (auto & j : metaNames) {
Value * v = i.queryMeta(j);
if (!v) continue;

View file

@ -1,4 +1,4 @@
ifeq ($(OS), Darwin)
ifdef HOST_DARWIN
programs += resolve-system-dependencies
endif