1
0
Fork 0
mirror of https://github.com/NixOS/nix synced 2025-07-05 16:31:47 +02:00

Merge remote-tracking branch 'upstream/master' into support-hardlinks-in-tarballs

This commit is contained in:
Robert Hensing 2024-07-11 11:43:02 +02:00
commit 86420753ec
583 changed files with 11313 additions and 16547 deletions

1
src/libutil/.version Symbolic link
View file

@ -0,0 +1 @@
../../.version

View file

@ -6,7 +6,7 @@
#include <strings.h> // for strcasecmp
#include "archive.hh"
#include "config.hh"
#include "config-global.hh"
#include "posix-source-accessor.hh"
#include "source-path.hh"
#include "file-system.hh"
@ -165,7 +165,7 @@ struct CaseInsensitiveCompare
};
static void parse(FileSystemObjectSink & sink, Source & source, const Path & path)
static void parse(FileSystemObjectSink & sink, Source & source, const CanonPath & path)
{
std::string s;
@ -246,7 +246,7 @@ static void parse(FileSystemObjectSink & sink, Source & source, const Path & pat
}
} else if (s == "node") {
if (name.empty()) throw badArchive("entry name missing");
parse(sink, source, path + "/" + name);
parse(sink, source, path / name);
} else
throw badArchive("unknown field " + s);
}
@ -290,11 +290,11 @@ void parseDump(FileSystemObjectSink & sink, Source & source)
}
if (version != narVersionMagic1)
throw badArchive("input doesn't look like a Nix archive");
parse(sink, source, "");
parse(sink, source, CanonPath::root);
}
void restorePath(const Path & path, Source & source)
void restorePath(const std::filesystem::path & path, Source & source)
{
RestoreSink sink;
sink.dstPath = path;

View file

@ -75,7 +75,7 @@ void dumpString(std::string_view s, Sink & sink);
void parseDump(FileSystemObjectSink & sink, Source & source);
void restorePath(const Path & path, Source & source);
void restorePath(const std::filesystem::path & path, Source & source);
/**
* Read a NAR from 'source' and write it to 'sink'.

View file

@ -0,0 +1 @@
../../build-utils-meson

View file

@ -0,0 +1,67 @@
#include "config-global.hh"
namespace nix {
bool GlobalConfig::set(const std::string & name, const std::string & value)
{
for (auto & config : *configRegistrations)
if (config->set(name, value))
return true;
unknownSettings.emplace(name, value);
return false;
}
void GlobalConfig::getSettings(std::map<std::string, SettingInfo> & res, bool overriddenOnly)
{
for (auto & config : *configRegistrations)
config->getSettings(res, overriddenOnly);
}
void GlobalConfig::resetOverridden()
{
for (auto & config : *configRegistrations)
config->resetOverridden();
}
nlohmann::json GlobalConfig::toJSON()
{
auto res = nlohmann::json::object();
for (const auto & config : *configRegistrations)
res.update(config->toJSON());
return res;
}
std::string GlobalConfig::toKeyValue()
{
std::string res;
std::map<std::string, Config::SettingInfo> settings;
globalConfig.getSettings(settings);
for (const auto & s : settings)
res += fmt("%s = %s\n", s.first, s.second.value);
return res;
}
void GlobalConfig::convertToArgs(Args & args, const std::string & category)
{
for (auto & config : *configRegistrations)
config->convertToArgs(args, category);
}
GlobalConfig globalConfig;
GlobalConfig::ConfigRegistrations * GlobalConfig::configRegistrations;
GlobalConfig::Register::Register(Config * config)
{
if (!configRegistrations)
configRegistrations = new ConfigRegistrations;
configRegistrations->emplace_back(config);
}
ExperimentalFeatureSettings experimentalFeatureSettings;
static GlobalConfig::Register rSettings(&experimentalFeatureSettings);
}

View file

@ -0,0 +1,33 @@
#pragma once
///@file
#include "config.hh"
namespace nix {
struct GlobalConfig : public AbstractConfig
{
typedef std::vector<Config *> ConfigRegistrations;
static ConfigRegistrations * configRegistrations;
bool set(const std::string & name, const std::string & value) override;
void getSettings(std::map<std::string, SettingInfo> & res, bool overriddenOnly = false) override;
void resetOverridden() override;
nlohmann::json toJSON() override;
std::string toKeyValue() override;
void convertToArgs(Args & args, const std::string & category) override;
struct Register
{
Register(Config * config);
};
};
extern GlobalConfig globalConfig;
}

View file

@ -91,7 +91,14 @@ void Config::getSettings(std::map<std::string, SettingInfo> & res, bool overridd
}
static void applyConfigInner(const std::string & contents, const std::string & path, std::vector<std::pair<std::string, std::string>> & parsedContents) {
/**
* Parse configuration in `contents`, and also the configuration files included from there, with their location specified relative to `path`.
*
* `contents` and `path` represent the file that is being parsed.
* The result is only an intermediate list of key-value pairs of strings.
* More parsing according to the settings-specific semantics is being done by `loadConfFile` in `libstore/globals.cc`.
*/
static void parseConfigFiles(const std::string & contents, const std::string & path, std::vector<std::pair<std::string, std::string>> & parsedContents) {
unsigned int pos = 0;
while (pos < contents.size()) {
@ -125,7 +132,7 @@ static void applyConfigInner(const std::string & contents, const std::string & p
if (pathExists(p)) {
try {
std::string includedContents = readFile(p);
applyConfigInner(includedContents, p, parsedContents);
parseConfigFiles(includedContents, p, parsedContents);
} catch (SystemError &) {
// TODO: Do we actually want to ignore this? Or is it better to fail?
}
@ -153,7 +160,7 @@ static void applyConfigInner(const std::string & contents, const std::string & p
void AbstractConfig::applyConfig(const std::string & contents, const std::string & path) {
std::vector<std::pair<std::string, std::string>> parsedContents;
applyConfigInner(contents, path, parsedContents);
parseConfigFiles(contents, path, parsedContents);
// First apply experimental-feature related settings
for (const auto & [name, value] : parsedContents)
@ -443,67 +450,6 @@ void OptionalPathSetting::operator =(const std::optional<Path> & v)
this->assign(v);
}
bool GlobalConfig::set(const std::string & name, const std::string & value)
{
for (auto & config : *configRegistrations)
if (config->set(name, value)) return true;
unknownSettings.emplace(name, value);
return false;
}
void GlobalConfig::getSettings(std::map<std::string, SettingInfo> & res, bool overriddenOnly)
{
for (auto & config : *configRegistrations)
config->getSettings(res, overriddenOnly);
}
void GlobalConfig::resetOverridden()
{
for (auto & config : *configRegistrations)
config->resetOverridden();
}
nlohmann::json GlobalConfig::toJSON()
{
auto res = nlohmann::json::object();
for (const auto & config : *configRegistrations)
res.update(config->toJSON());
return res;
}
std::string GlobalConfig::toKeyValue()
{
std::string res;
std::map<std::string, Config::SettingInfo> settings;
globalConfig.getSettings(settings);
for (const auto & s : settings)
res += fmt("%s = %s\n", s.first, s.second.value);
return res;
}
void GlobalConfig::convertToArgs(Args & args, const std::string & category)
{
for (auto & config : *configRegistrations)
config->convertToArgs(args, category);
}
GlobalConfig globalConfig;
GlobalConfig::ConfigRegistrations * GlobalConfig::configRegistrations;
GlobalConfig::Register::Register(Config * config)
{
if (!configRegistrations)
configRegistrations = new ConfigRegistrations;
configRegistrations->emplace_back(config);
}
ExperimentalFeatureSettings experimentalFeatureSettings;
static GlobalConfig::Register rSettings(&experimentalFeatureSettings);
bool ExperimentalFeatureSettings::isEnabled(const ExperimentalFeature & feature) const
{
auto & f = experimentalFeatures.get();

View file

@ -375,31 +375,6 @@ public:
void operator =(const std::optional<Path> & v);
};
struct GlobalConfig : public AbstractConfig
{
typedef std::vector<Config*> ConfigRegistrations;
static ConfigRegistrations * configRegistrations;
bool set(const std::string & name, const std::string & value) override;
void getSettings(std::map<std::string, SettingInfo> & res, bool overriddenOnly = false) override;
void resetOverridden() override;
nlohmann::json toJSON() override;
std::string toKeyValue() override;
void convertToArgs(Args & args, const std::string & category) override;
struct Register
{
Register(Config * config);
};
};
extern GlobalConfig globalConfig;
struct ExperimentalFeatureSettings : Config {

View file

@ -155,6 +155,7 @@ public:
: err(e)
{ }
/** The error message without "error: " prefixed to it. */
std::string message() {
return err.msg.str();
}

View file

@ -66,7 +66,7 @@ constexpr std::array<ExperimentalFeatureDetails, numXpFeatures> xpFeatureDetails
an impure derivation cannot also be
[content-addressed](#xp-feature-ca-derivations).
This is a more explicit alternative to using [`builtins.currentTime`](@docroot@/language/builtin-constants.md#builtins-currentTime).
This is a more explicit alternative to using [`builtins.currentTime`](@docroot@/language/builtins.md#builtins-currentTime).
)",
.trackingUrl = "https://github.com/NixOS/nix/milestone/42",
},

View file

@ -10,7 +10,7 @@ static std::optional<FileSerialisationMethod> parseFileSerialisationMethodOpt(st
if (input == "flat") {
return FileSerialisationMethod::Flat;
} else if (input == "nar") {
return FileSerialisationMethod::Recursive;
return FileSerialisationMethod::NixArchive;
} else {
return std::nullopt;
}
@ -45,7 +45,7 @@ std::string_view renderFileSerialisationMethod(FileSerialisationMethod method)
switch (method) {
case FileSerialisationMethod::Flat:
return "flat";
case FileSerialisationMethod::Recursive:
case FileSerialisationMethod::NixArchive:
return "nar";
default:
assert(false);
@ -57,7 +57,7 @@ std::string_view renderFileIngestionMethod(FileIngestionMethod method)
{
switch (method) {
case FileIngestionMethod::Flat:
case FileIngestionMethod::Recursive:
case FileIngestionMethod::NixArchive:
return renderFileSerialisationMethod(
static_cast<FileSerialisationMethod>(method));
case FileIngestionMethod::Git:
@ -78,7 +78,7 @@ void dumpPath(
case FileSerialisationMethod::Flat:
path.readFile(sink);
break;
case FileSerialisationMethod::Recursive:
case FileSerialisationMethod::NixArchive:
path.dumpPath(sink, filter);
break;
}
@ -94,7 +94,7 @@ void restorePath(
case FileSerialisationMethod::Flat:
writeFile(path, source);
break;
case FileSerialisationMethod::Recursive:
case FileSerialisationMethod::NixArchive:
restorePath(path, source);
break;
}
@ -119,7 +119,7 @@ std::pair<Hash, std::optional<uint64_t>> hashPath(
{
switch (method) {
case FileIngestionMethod::Flat:
case FileIngestionMethod::Recursive: {
case FileIngestionMethod::NixArchive: {
auto res = hashPath(path, (FileSerialisationMethod) method, ht, filter);
return {res.first, {res.second}};
}

View file

@ -35,14 +35,14 @@ enum struct FileSerialisationMethod : uint8_t {
* See `file-system-object/content-address.md#serial-nix-archive` in
* the manual.
*/
Recursive,
NixArchive,
};
/**
* Parse a `FileSerialisationMethod` by name. Choice of:
*
* - `flat`: `FileSerialisationMethod::Flat`
* - `nar`: `FileSerialisationMethod::Recursive`
* - `nar`: `FileSerialisationMethod::NixArchive`
*
* Opposite of `renderFileSerialisationMethod`.
*/
@ -107,16 +107,18 @@ enum struct FileIngestionMethod : uint8_t {
Flat,
/**
* Hash `FileSerialisationMethod::Recursive` serialisation.
* Hash `FileSerialisationMethod::NixArchive` serialisation.
*
* See `file-system-object/content-address.md#serial-flat` in the
* manual.
*/
Recursive,
NixArchive,
/**
* Git hashing.
*
* Part of `ExperimentalFeature::GitHashing`.
*
* See `file-system-object/content-address.md#serial-git` in the
* manual.
*/
@ -127,7 +129,7 @@ enum struct FileIngestionMethod : uint8_t {
* Parse a `FileIngestionMethod` by name. Choice of:
*
* - `flat`: `FileIngestionMethod::Flat`
* - `nar`: `FileIngestionMethod::Recursive`
* - `nar`: `FileIngestionMethod::NixArchive`
* - `git`: `FileIngestionMethod::Git`
*
* Opposite of `renderFileIngestionMethod`.

View file

@ -140,6 +140,7 @@ public:
};
#ifndef _WIN32 // Not needed on Windows, where we don't fork
namespace unix {
/**
* Close all file descriptors except those listed in the given set.
@ -152,13 +153,16 @@ void closeMostFDs(const std::set<Descriptor> & exceptions);
*/
void closeOnExec(Descriptor fd);
} // namespace unix
#endif
#ifdef _WIN32
# if _WIN32_WINNT >= 0x0600
#if defined(_WIN32) && _WIN32_WINNT >= 0x0600
namespace windows {
Path handleToPath(Descriptor handle);
std::wstring handleToFileName(Descriptor handle);
# endif
} // namespace windows
#endif
MakeError(EndOfFile, Error);

View file

@ -412,31 +412,19 @@ void deletePath(const fs::path & path)
deletePath(path, dummy);
}
Paths createDirs(const Path & path)
void createDir(const Path & path, mode_t mode)
{
Paths created;
if (path == "/") return created;
if (mkdir(path.c_str(), mode) == -1)
throw SysError("creating directory '%1%'", path);
}
struct stat st;
if (STAT(path.c_str(), &st) == -1) {
created = createDirs(dirOf(path));
if (mkdir(path.c_str()
#ifndef _WIN32 // TODO abstract mkdir perms for Windows
, 0777
#endif
) == -1 && errno != EEXIST)
throw SysError("creating directory '%1%'", path);
st = STAT(path);
created.push_back(path);
void createDirs(const Path & path)
{
try {
fs::create_directories(path);
} catch (fs::filesystem_error & e) {
throw SysError("creating directory '%1%'", path);
}
if (S_ISLNK(st.st_mode) && stat(path.c_str(), &st) == -1)
throw SysError("statting symlink '%1%'", path);
if (!S_ISDIR(st.st_mode)) throw Error("'%1%' is not a directory", path);
return created;
}
@ -546,7 +534,7 @@ std::pair<AutoCloseFD, Path> createTempFile(const Path & prefix)
if (!fd)
throw SysError("creating temporary file '%s'", tmpl);
#ifndef _WIN32
closeOnExec(fd.get());
unix::closeOnExec(fd.get());
#endif
return {std::move(fd), tmpl};
}
@ -574,29 +562,69 @@ void replaceSymlink(const Path & target, const Path & link)
}
}
#ifndef _WIN32
static void setWriteTime(const fs::path & p, const struct stat & st)
void setWriteTime(
const std::filesystem::path & path,
time_t accessedTime,
time_t modificationTime,
std::optional<bool> optIsSymlink)
{
struct timeval times[2];
times[0] = {
.tv_sec = st.st_atime,
.tv_usec = 0,
#ifndef _WIN32
struct timeval times[2] = {
{
.tv_sec = accessedTime,
.tv_usec = 0,
},
{
.tv_sec = modificationTime,
.tv_usec = 0,
},
};
times[1] = {
.tv_sec = st.st_mtime,
.tv_usec = 0,
};
if (lutimes(p.c_str(), times) != 0)
throw SysError("changing modification time of '%s'", p);
}
#endif
auto nonSymlink = [&]{
bool isSymlink = optIsSymlink
? *optIsSymlink
: fs::is_symlink(path);
if (!isSymlink) {
#ifdef _WIN32
// FIXME use `fs::last_write_time`.
//
// Would be nice to use std::filesystem unconditionally, but
// doesn't support access time just modification time.
//
// System clock vs File clock issues also make that annoying.
warn("Changing file times is not yet implemented on Windows, path is '%s'", path);
#else
if (utimes(path.c_str(), times) == -1) {
throw SysError("changing modification time of '%s' (not a symlink)", path);
}
#endif
} else {
throw Error("Cannot modification time of symlink '%s'", path);
}
};
#if HAVE_LUTIMES
if (lutimes(path.c_str(), times) == -1) {
if (errno == ENOSYS)
nonSymlink();
else
throw SysError("changing modification time of '%s'", path);
}
#else
nonSymlink();
#endif
}
void setWriteTime(const fs::path & path, const struct stat & st)
{
setWriteTime(path, st.st_atime, st.st_mtime, S_ISLNK(st.st_mode));
}
void copyFile(const fs::path & from, const fs::path & to, bool andDelete)
{
#ifndef _WIN32
// TODO: Rewrite the `is_*` to use `symlink_status()`
auto statOfFrom = lstat(from.c_str());
#endif
auto fromStatus = fs::symlink_status(from);
// Mark the directory as writable so that we can delete its children
@ -616,9 +644,7 @@ void copyFile(const fs::path & from, const fs::path & to, bool andDelete)
throw Error("file '%s' has an unsupported type", from);
}
#ifndef _WIN32
setWriteTime(to, statOfFrom);
#endif
setWriteTime(to, lstat(from.string().c_str()));
if (andDelete) {
if (!fs::is_symlink(fromStatus))
fs::permissions(from, fs::perms::owner_write, fs::perm_options::add | fs::perm_options::nofollow);

View file

@ -148,15 +148,43 @@ void deletePath(const std::filesystem::path & path);
void deletePath(const std::filesystem::path & path, uint64_t & bytesFreed);
/**
* Create a directory and all its parents, if necessary. Returns the
* list of created directories, in order of creation.
* Create a directory and all its parents, if necessary.
*/
Paths createDirs(const Path & path);
inline Paths createDirs(PathView path)
void createDirs(const Path & path);
inline void createDirs(PathView path)
{
return createDirs(Path(path));
}
/**
* Create a single directory.
*/
void createDir(const Path & path, mode_t mode = 0755);
/**
* Set the access and modification times of the given path, not
* following symlinks.
*
* @param accessTime Specified in seconds.
*
* @param modificationTime Specified in seconds.
*
* @param isSymlink Whether the file in question is a symlink. Used for
* fallback code where we don't have `lutimes` or similar. if
* `std::optional` is passed, the information will be recomputed if it
* is needed. Race conditions are possible so be careful!
*/
void setWriteTime(
const std::filesystem::path & path,
time_t accessedTime,
time_t modificationTime,
std::optional<bool> isSymlink = std::nullopt);
/**
* Convenience wrapper that takes all arguments from the `struct stat`.
*/
void setWriteTime(const std::filesystem::path & path, const struct stat & st);
/**
* Create a symlink.
*/

View file

@ -111,6 +111,8 @@ std::ostream & operator<<(std::ostream & out, const Magenta<T> & y)
/**
* Values wrapped in this class are printed without coloring.
*
* Specifically, the color is reset to normal before printing the value.
*
* By default, arguments to `HintFmt` are printed in magenta (see `Magenta`).
*/
template <class T>

View file

@ -1,7 +1,7 @@
#include <fcntl.h>
#include "error.hh"
#include "config.hh"
#include "config-global.hh"
#include "fs-sink.hh"
#if _WIN32
@ -14,7 +14,7 @@ namespace nix {
void copyRecursive(
SourceAccessor & accessor, const CanonPath & from,
FileSystemObjectSink & sink, const Path & to)
FileSystemObjectSink & sink, const CanonPath & to)
{
auto stat = accessor.lstat(from);
@ -43,7 +43,7 @@ void copyRecursive(
for (auto & [name, _] : accessor.readDirectory(from)) {
copyRecursive(
accessor, from / name,
sink, to + "/" + name);
sink, to / name);
break;
}
break;
@ -69,17 +69,9 @@ static RestoreSinkSettings restoreSinkSettings;
static GlobalConfig::Register r1(&restoreSinkSettings);
void RestoreSink::createDirectory(const Path & path)
void RestoreSink::createDirectory(const CanonPath & path)
{
Path p = dstPath + path;
if (
#ifndef _WIN32 // TODO abstract mkdir perms for Windows
mkdir(p.c_str(), 0777) == -1
#else
!CreateDirectoryW(pathNG(p).c_str(), NULL)
#endif
)
throw NativeSysError("creating directory '%1%'", p);
std::filesystem::create_directory(dstPath / path.rel());
};
struct RestoreRegularFile : CreateRegularFileSink {
@ -90,13 +82,22 @@ struct RestoreRegularFile : CreateRegularFileSink {
void preallocateContents(uint64_t size) override;
};
void RestoreSink::createRegularFile(const Path & path, std::function<void(CreateRegularFileSink &)> func)
static std::filesystem::path append(const std::filesystem::path & src, const CanonPath & path)
{
Path p = dstPath + path;
auto dst = src;
if (!path.rel().empty())
dst /= path.rel();
return dst;
}
void RestoreSink::createRegularFile(const CanonPath & path, std::function<void(CreateRegularFileSink &)> func)
{
auto p = append(dstPath, path);
RestoreRegularFile crf;
crf.fd =
#ifdef _WIN32
CreateFileW(pathNG(path).c_str(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL)
CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL)
#else
open(p.c_str(), O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC, 0666)
#endif
@ -141,14 +142,14 @@ void RestoreRegularFile::operator () (std::string_view data)
writeFull(fd.get(), data);
}
void RestoreSink::createSymlink(const Path & path, const std::string & target)
void RestoreSink::createSymlink(const CanonPath & path, const std::string & target)
{
Path p = dstPath + path;
auto p = append(dstPath, path);
nix::createSymlink(target, p);
}
void RegularFileSink::createRegularFile(const Path & path, std::function<void(CreateRegularFileSink &)> func)
void RegularFileSink::createRegularFile(const CanonPath & path, std::function<void(CreateRegularFileSink &)> func)
{
struct CRF : CreateRegularFileSink {
RegularFileSink & back;
@ -163,7 +164,7 @@ void RegularFileSink::createRegularFile(const Path & path, std::function<void(Cr
}
void NullFileSystemObjectSink::createRegularFile(const Path & path, std::function<void(CreateRegularFileSink &)> func)
void NullFileSystemObjectSink::createRegularFile(const CanonPath & path, std::function<void(CreateRegularFileSink &)> func)
{
struct : CreateRegularFileSink {
void operator () (std::string_view data) override {}

View file

@ -28,17 +28,17 @@ struct FileSystemObjectSink
{
virtual ~FileSystemObjectSink() = default;
virtual void createDirectory(const Path & path) = 0;
virtual void createDirectory(const CanonPath & path) = 0;
/**
* This function in general is no re-entrant. Only one file can be
* written at a time.
*/
virtual void createRegularFile(
const Path & path,
const CanonPath & path,
std::function<void(CreateRegularFileSink &)>) = 0;
virtual void createSymlink(const Path & path, const std::string & target) = 0;
virtual void createSymlink(const CanonPath & path, const std::string & target) = 0;
};
/**
@ -59,17 +59,17 @@ struct ExtendedFileSystemObjectSink : virtual FileSystemObjectSink
*/
void copyRecursive(
SourceAccessor & accessor, const CanonPath & sourcePath,
FileSystemObjectSink & sink, const Path & destPath);
FileSystemObjectSink & sink, const CanonPath & destPath);
/**
* Ignore everything and do nothing
*/
struct NullFileSystemObjectSink : FileSystemObjectSink
{
void createDirectory(const Path & path) override { }
void createSymlink(const Path & path, const std::string & target) override { }
void createDirectory(const CanonPath & path) override { }
void createSymlink(const CanonPath & path, const std::string & target) override { }
void createRegularFile(
const Path & path,
const CanonPath & path,
std::function<void(CreateRegularFileSink &)>) override;
};
@ -78,15 +78,15 @@ struct NullFileSystemObjectSink : FileSystemObjectSink
*/
struct RestoreSink : FileSystemObjectSink
{
Path dstPath;
std::filesystem::path dstPath;
void createDirectory(const Path & path) override;
void createDirectory(const CanonPath & path) override;
void createRegularFile(
const Path & path,
const CanonPath & path,
std::function<void(CreateRegularFileSink &)>) override;
void createSymlink(const Path & path, const std::string & target) override;
void createSymlink(const CanonPath & path, const std::string & target) override;
};
/**
@ -101,18 +101,18 @@ struct RegularFileSink : FileSystemObjectSink
RegularFileSink(Sink & sink) : sink(sink) { }
void createDirectory(const Path & path) override
void createDirectory(const CanonPath & path) override
{
regular = false;
}
void createSymlink(const Path & path, const std::string & target) override
void createSymlink(const CanonPath & path, const std::string & target) override
{
regular = false;
}
void createRegularFile(
const Path & path,
const CanonPath & path,
std::function<void(CreateRegularFileSink &)>) override;
};

View file

@ -53,7 +53,7 @@ static std::string getString(Source & source, int n)
void parseBlob(
FileSystemObjectSink & sink,
const Path & sinkPath,
const CanonPath & sinkPath,
Source & source,
BlobMode blobMode,
const ExperimentalFeatureSettings & xpSettings)
@ -116,7 +116,7 @@ void parseBlob(
void parseTree(
FileSystemObjectSink & sink,
const Path & sinkPath,
const CanonPath & sinkPath,
Source & source,
std::function<SinkHook> hook,
const ExperimentalFeatureSettings & xpSettings)
@ -147,7 +147,7 @@ void parseTree(
Hash hash(HashAlgorithm::SHA1);
std::copy(hashs.begin(), hashs.end(), hash.hash);
hook(name, TreeEntry {
hook(CanonPath{name}, TreeEntry {
.mode = mode,
.hash = hash,
});
@ -171,7 +171,7 @@ ObjectType parseObjectType(
void parse(
FileSystemObjectSink & sink,
const Path & sinkPath,
const CanonPath & sinkPath,
Source & source,
BlobMode rootModeIfBlob,
std::function<SinkHook> hook,
@ -208,7 +208,7 @@ std::optional<Mode> convertMode(SourceAccessor::Type type)
void restore(FileSystemObjectSink & sink, Source & source, std::function<RestoreHook> hook)
{
parse(sink, "", source, BlobMode::Regular, [&](Path name, TreeEntry entry) {
parse(sink, CanonPath::root, source, BlobMode::Regular, [&](CanonPath name, TreeEntry entry) {
auto [accessor, from] = hook(entry.hash);
auto stat = accessor->lstat(from);
auto gotOpt = convertMode(stat.type);

View file

@ -64,7 +64,7 @@ using Tree = std::map<std::string, TreeEntry>;
* Implementations may seek to memoize resources (bandwidth, storage,
* etc.) for the same Git hash.
*/
using SinkHook = void(const Path & name, TreeEntry entry);
using SinkHook = void(const CanonPath & name, TreeEntry entry);
/**
* Parse the "blob " or "tree " prefix.
@ -89,13 +89,13 @@ enum struct BlobMode : RawMode
};
void parseBlob(
FileSystemObjectSink & sink, const Path & sinkPath,
FileSystemObjectSink & sink, const CanonPath & sinkPath,
Source & source,
BlobMode blobMode,
const ExperimentalFeatureSettings & xpSettings = experimentalFeatureSettings);
void parseTree(
FileSystemObjectSink & sink, const Path & sinkPath,
FileSystemObjectSink & sink, const CanonPath & sinkPath,
Source & source,
std::function<SinkHook> hook,
const ExperimentalFeatureSettings & xpSettings = experimentalFeatureSettings);
@ -108,7 +108,7 @@ void parseTree(
* a blob, this is ignored.
*/
void parse(
FileSystemObjectSink & sink, const Path & sinkPath,
FileSystemObjectSink & sink, const CanonPath & sinkPath,
Source & source,
BlobMode rootModeIfBlob,
std::function<SinkHook> hook,

View file

@ -52,11 +52,11 @@ bool Hash::operator == (const Hash & h2) const
std::strong_ordering Hash::operator <=> (const Hash & h) const
{
if (auto cmp = algo <=> h.algo; cmp != 0) return cmp;
if (auto cmp = hashSize <=> h.hashSize; cmp != 0) return cmp;
for (unsigned int i = 0; i < hashSize; i++) {
if (auto cmp = hash[i] <=> h.hash[i]; cmp != 0) return cmp;
}
if (auto cmp = algo <=> h.algo; cmp != 0) return cmp;
return std::strong_ordering::equivalent;
}

View file

@ -0,0 +1,11 @@
sources += files(
'cgroup.cc',
'namespaces.cc',
)
include_dirs += include_directories('.')
headers += files(
'cgroup.hh',
'namespaces.hh',
)

View file

@ -3,7 +3,7 @@
#include "environment-variables.hh"
#include "terminal.hh"
#include "util.hh"
#include "config.hh"
#include "config-global.hh"
#include "source-path.hh"
#include "position.hh"

View file

@ -124,9 +124,9 @@ SourcePath MemorySourceAccessor::addFile(CanonPath path, std::string && contents
using File = MemorySourceAccessor::File;
void MemorySink::createDirectory(const Path & path)
void MemorySink::createDirectory(const CanonPath & path)
{
auto * f = dst.open(CanonPath{path}, File { File::Directory { } });
auto * f = dst.open(path, File { File::Directory { } });
if (!f)
throw Error("file '%s' cannot be made because some parent file is not a directory", path);
@ -146,9 +146,9 @@ struct CreateMemoryRegularFile : CreateRegularFileSink {
void preallocateContents(uint64_t size) override;
};
void MemorySink::createRegularFile(const Path & path, std::function<void(CreateRegularFileSink &)> func)
void MemorySink::createRegularFile(const CanonPath & path, std::function<void(CreateRegularFileSink &)> func)
{
auto * f = dst.open(CanonPath{path}, File { File::Regular {} });
auto * f = dst.open(path, File { File::Regular {} });
if (!f)
throw Error("file '%s' cannot be made because some parent file is not a directory", path);
if (auto * rp = std::get_if<File::Regular>(&f->raw)) {
@ -173,9 +173,9 @@ void CreateMemoryRegularFile::operator () (std::string_view data)
regularFile.contents += data;
}
void MemorySink::createSymlink(const Path & path, const std::string & target)
void MemorySink::createSymlink(const CanonPath & path, const std::string & target)
{
auto * f = dst.open(CanonPath{path}, File { File::Symlink { } });
auto * f = dst.open(path, File { File::Symlink { } });
if (!f)
throw Error("file '%s' cannot be made because some parent file is not a directory", path);
if (auto * s = std::get_if<File::Symlink>(&f->raw))

View file

@ -81,13 +81,13 @@ struct MemorySink : FileSystemObjectSink
MemorySink(MemorySourceAccessor & dst) : dst(dst) { }
void createDirectory(const Path & path) override;
void createDirectory(const CanonPath & path) override;
void createRegularFile(
const Path & path,
const CanonPath & path,
std::function<void(CreateRegularFileSink &)>) override;
void createSymlink(const Path & path, const std::string & target) override;
void createSymlink(const CanonPath & path, const std::string & target) override;
};
}

265
src/libutil/meson.build Normal file
View file

@ -0,0 +1,265 @@
project('nix-util', 'cpp',
version : files('.version'),
default_options : [
'cpp_std=c++2a',
# TODO(Qyriad): increase the warning level
'warning_level=1',
'debug=true',
'optimization=2',
'errorlogs=true', # Please print logs for tests that fail
],
meson_version : '>= 1.1',
license : 'LGPL-2.1-or-later',
)
cxx = meson.get_compiler('cpp')
subdir('build-utils-meson/deps-lists')
configdata = configuration_data()
deps_private_maybe_subproject = [
]
deps_public_maybe_subproject = [
]
subdir('build-utils-meson/subprojects')
# Check for each of these functions, and create a define like `#define
# HAVE_LUTIMES 1`. The `#define` is unconditional, 0 for not found and 1
# for found. One therefore uses it with `#if` not `#ifdef`.
check_funcs = [
# Optionally used for changing the mtime of symlinks.
'lutimes',
# Optionally used for creating pipes on Unix
'pipe2',
# Optionally used to preallocate files to be large enough before
# writing to them.
'posix_fallocate',
# Optionally used to get more information about processes failing due
# to a signal on Unix.
'strsignal',
# Optionally used to try to close more file descriptors (e.g. before
# forking) on Unix.
'sysconf',
]
foreach funcspec : check_funcs
define_name = 'HAVE_' + funcspec.underscorify().to_upper()
define_value = cxx.has_function(funcspec).to_int()
configdata.set(define_name, define_value)
endforeach
subdir('build-utils-meson/threads')
if host_machine.system() == 'windows'
socket = cxx.find_library('ws2_32')
deps_other += socket
elif host_machine.system() == 'sunos'
socket = cxx.find_library('socket')
network_service_library = cxx.find_library('nsl')
deps_other += [socket, network_service_library]
endif
boost = dependency(
'boost',
modules : ['context', 'coroutine'],
)
# boost is a public dependency, but not a pkg-config dependency unfortunately, so we
# put in `deps_other`.
deps_other += boost
openssl = dependency(
'libcrypto',
'openssl',
version : '>= 1.1.1',
)
deps_private += openssl
libarchive = dependency('libarchive', version : '>= 3.1.2')
deps_public += libarchive
if get_option('default_library') == 'static'
# Workaround until https://github.com/libarchive/libarchive/issues/1446 is fixed
add_project_arguments('-lz', language : 'cpp')
endif
sodium = dependency('libsodium', 'sodium')
deps_private += sodium
brotli = [
dependency('libbrotlicommon'),
dependency('libbrotlidec'),
dependency('libbrotlienc'),
]
deps_private += brotli
cpuid_required = get_option('cpuid')
if host_machine.cpu_family() != 'x86_64' and cpuid_required.enabled()
warning('Force-enabling seccomp on non-x86_64 does not make sense')
endif
cpuid = dependency('libcpuid', 'cpuid', required : cpuid_required)
configdata.set('HAVE_LIBCPUID', cpuid.found().to_int())
deps_private += cpuid
nlohmann_json = dependency('nlohmann_json', version : '>= 3.9')
deps_public += nlohmann_json
config_h = configure_file(
configuration : configdata,
output : 'config-util.hh',
)
add_project_arguments(
# TODO(Qyriad): Yes this is how the autoconf+Make system did it.
# It would be nice for our headers to be idempotent instead.
'-include', 'config-util.hh',
language : 'cpp',
)
subdir('build-utils-meson/diagnostics')
sources = files(
'archive.cc',
'args.cc',
'canon-path.cc',
'compression.cc',
'compute-levels.cc',
'config.cc',
'config-global.cc',
'current-process.cc',
'english.cc',
'environment-variables.cc',
'error.cc',
'exit.cc',
'experimental-features.cc',
'file-content-address.cc',
'file-descriptor.cc',
'file-system.cc',
'fs-sink.cc',
'git.cc',
'hash.cc',
'hilite.cc',
'json-utils.cc',
'logging.cc',
'memory-source-accessor.cc',
'position.cc',
'posix-source-accessor.cc',
'references.cc',
'serialise.cc',
'signature/local-keys.cc',
'signature/signer.cc',
'source-accessor.cc',
'source-path.cc',
'suggestions.cc',
'tarfile.cc',
'terminal.cc',
'thread-pool.cc',
'unix-domain-socket.cc',
'url.cc',
'users.cc',
'util.cc',
'xml-writer.cc',
)
include_dirs = [include_directories('.')]
headers = [config_h] + files(
'abstract-setting-to-json.hh',
'ansicolor.hh',
'archive.hh',
'args.hh',
'args/root.hh',
'callback.hh',
'canon-path.hh',
'chunked-vector.hh',
'closure.hh',
'comparator.hh',
'compression.hh',
'compute-levels.hh',
'config-global.hh',
'config-impl.hh',
'config.hh',
'current-process.hh',
'english.hh',
'environment-variables.hh',
'error.hh',
'exit.hh',
'experimental-features.hh',
'file-content-address.hh',
'file-descriptor.hh',
'file-path-impl.hh',
'file-path.hh',
'file-system.hh',
'finally.hh',
'fmt.hh',
'fs-sink.hh',
'git.hh',
'hash.hh',
'hilite.hh',
'json-impls.hh',
'json-utils.hh',
'logging.hh',
'lru-cache.hh',
'memory-source-accessor.hh',
'muxable-pipe.hh',
'pool.hh',
'position.hh',
'posix-source-accessor.hh',
'processes.hh',
'ref.hh',
'references.hh',
'regex-combinators.hh',
'repair-flag.hh',
'serialise.hh',
'signals.hh',
'signature/local-keys.hh',
'signature/signer.hh',
'source-accessor.hh',
'source-path.hh',
'split.hh',
'suggestions.hh',
'sync.hh',
'tarfile.hh',
'terminal.hh',
'thread-pool.hh',
'topo-sort.hh',
'types.hh',
'unix-domain-socket.hh',
'url-parts.hh',
'url.hh',
'users.hh',
'util.hh',
'variant-wrapper.hh',
'xml-writer.hh',
)
if host_machine.system() == 'linux'
subdir('linux')
endif
if host_machine.system() == 'windows'
subdir('windows')
else
subdir('unix')
endif
subdir('build-utils-meson/export-all-symbols')
this_library = library(
'nixutil',
sources,
dependencies : deps_public + deps_private + deps_other,
include_directories : include_dirs,
link_args: linker_export_flags,
prelink : true, # For C++ static initializers
install : true,
)
install_headers(headers, subdir : 'nix', preserve_path : true)
libraries_private = []
if host_machine.system() == 'windows'
# `libraries_private` cannot contain ad-hoc dependencies (from
# `find_library), so we need to do this manually
libraries_private += ['-lws2_32']
endif
subdir('build-utils-meson/export')

View file

@ -0,0 +1,5 @@
# vim: filetype=meson
option('cpuid', type : 'feature',
description : 'determine microarchitecture levels with libcpuid (only relevant on x86_64)',
)

103
src/libutil/package.nix Normal file
View file

@ -0,0 +1,103 @@
{ lib
, stdenv
, mkMesonDerivation
, releaseTools
, meson
, ninja
, pkg-config
, boost
, brotli
, libarchive
, libcpuid
, libsodium
, nlohmann_json
, openssl
# Configuration Options
, version
}:
let
inherit (lib) fileset;
in
mkMesonDerivation (finalAttrs: {
pname = "nix-util";
inherit version;
workDir = ./.;
fileset = fileset.unions [
../../build-utils-meson
./build-utils-meson
../../.version
./.version
./meson.build
./meson.options
./linux/meson.build
./unix/meson.build
./windows/meson.build
(fileset.fileFilter (file: file.hasExt "cc") ./.)
(fileset.fileFilter (file: file.hasExt "hh") ./.)
];
outputs = [ "out" "dev" ];
nativeBuildInputs = [
meson
ninja
pkg-config
];
buildInputs = [
brotli
libsodium
openssl
] ++ lib.optional stdenv.hostPlatform.isx86_64 libcpuid
;
propagatedBuildInputs = [
boost
libarchive
nlohmann_json
];
preConfigure =
# "Inline" .version so it's not a symlink, and includes the suffix.
# Do the meson utils, without modification.
#
# TODO: change release process to add `pre` in `.version`, remove it
# before tagging, and restore after.
''
chmod u+w ./.version
echo ${version} > ../../.version
'';
mesonFlags = [
(lib.mesonEnable "cpuid" stdenv.hostPlatform.isx86_64)
];
env = {
# Needed for Meson to find Boost.
# https://github.com/NixOS/nixpkgs/issues/86131.
BOOST_INCLUDEDIR = "${lib.getDev boost}/include";
BOOST_LIBRARYDIR = "${lib.getLib boost}/lib";
} // lib.optionalAttrs (stdenv.isLinux && !(stdenv.hostPlatform.isStatic && stdenv.system == "aarch64-linux")) {
LDFLAGS = "-fuse-ld=gold";
};
enableParallelBuilding = true;
separateDebugInfo = !stdenv.hostPlatform.isStatic;
strictDeps = true;
hardeningDisable = lib.optional stdenv.hostPlatform.isStatic "pie";
meta = {
platforms = lib.platforms.unix ++ lib.platforms.windows;
};
})

View file

@ -3,6 +3,7 @@
#include "types.hh"
#include "error.hh"
#include "file-descriptor.hh"
#include "logging.hh"
#include "ansicolor.hh"
@ -23,26 +24,36 @@ namespace nix {
struct Sink;
struct Source;
#ifndef _WIN32
class Pid
{
#ifndef _WIN32
pid_t pid = -1;
bool separatePG = false;
int killSignal = SIGKILL;
#else
AutoCloseFD pid = INVALID_DESCRIPTOR;
#endif
public:
Pid();
#ifndef _WIN32
Pid(pid_t pid);
~Pid();
void operator =(pid_t pid);
operator pid_t();
#else
Pid(AutoCloseFD pid);
void operator =(AutoCloseFD pid);
#endif
~Pid();
int kill();
int wait();
// TODO: Implement for Windows
#ifndef _WIN32
void setSeparatePG(bool separatePG);
void setKillSignal(int signal);
pid_t release();
};
#endif
};
#ifndef _WIN32

View file

@ -67,6 +67,17 @@ int getArchiveFilterCodeByName(const std::string & method)
return code;
}
static void enableSupportedFormats(struct archive * archive)
{
archive_read_support_format_tar(archive);
archive_read_support_format_zip(archive);
/* Enable support for empty files so we don't throw an exception
for empty HTTP 304 "Not modified" responses. See
downloadTarball(). */
archive_read_support_format_empty(archive);
}
TarArchive::TarArchive(Source & source, bool raw, std::optional<std::string> compression_method)
: archive{archive_read_new()}
, source{&source}
@ -78,9 +89,9 @@ TarArchive::TarArchive(Source & source, bool raw, std::optional<std::string> com
archive_read_support_filter_by_code(archive, getArchiveFilterCodeByName(*compression_method));
}
if (!raw) {
archive_read_support_format_all(archive);
} else {
if (!raw)
enableSupportedFormats(archive);
else {
archive_read_support_format_raw(archive);
archive_read_support_format_empty(archive);
}
@ -96,7 +107,7 @@ TarArchive::TarArchive(const Path & path)
, buffer(defaultBufferSize)
{
archive_read_support_filter_all(archive);
archive_read_support_format_all(archive);
enableSupportedFormats(archive);
archive_read_set_option(archive, NULL, "mac-ext", NULL);
check(archive_read_open_filename(archive, path.c_str(), 16384), "failed to open archive: %s");
}
@ -176,6 +187,7 @@ time_t unpackTarfileToSink(TarArchive & archive, ExtendedFileSystemObjectSink &
auto path = archive_entry_pathname(entry);
if (!path)
throw Error("cannot get archive member name: %s", archive_error_string(archive.archive));
auto cpath = CanonPath{path};
if (r == ARCHIVE_WARN)
warn(archive_error_string(archive.archive));
else
@ -191,11 +203,11 @@ time_t unpackTarfileToSink(TarArchive & archive, ExtendedFileSystemObjectSink &
switch (auto type = archive_entry_filetype(entry)) {
case AE_IFDIR:
parseSink.createDirectory(path);
parseSink.createDirectory(cpath);
break;
case AE_IFREG: {
parseSink.createRegularFile(path, [&](auto & crf) {
parseSink.createRegularFile(cpath, [&](auto & crf) {
if (archive_entry_mode(entry) & S_IXUSR)
crf.isExecutable();
@ -219,7 +231,7 @@ time_t unpackTarfileToSink(TarArchive & archive, ExtendedFileSystemObjectSink &
case AE_IFLNK: {
auto target = archive_entry_symlink(entry);
parseSink.createSymlink(path, target);
parseSink.createSymlink(cpath, target);
break;
}

View file

@ -24,7 +24,7 @@ AutoCloseFD createUnixDomainSocket()
if (!fdSocket)
throw SysError("cannot create Unix domain socket");
#ifndef _WIN32
closeOnExec(fdSocket.get());
unix::closeOnExec(fdSocket.get());
#endif
return fdSocket;
}

View file

@ -110,8 +110,8 @@ void Pipe::create()
if (pipe2(fds, O_CLOEXEC) != 0) throw SysError("creating pipe");
#else
if (pipe(fds) != 0) throw SysError("creating pipe");
closeOnExec(fds[0]);
closeOnExec(fds[1]);
unix::closeOnExec(fds[0]);
unix::closeOnExec(fds[1]);
#endif
readSide = fds[0];
writeSide = fds[1];
@ -120,7 +120,7 @@ void Pipe::create()
//////////////////////////////////////////////////////////////////////
void closeMostFDs(const std::set<int> & exceptions)
void unix::closeMostFDs(const std::set<int> & exceptions)
{
#if __linux__
try {
@ -139,14 +139,16 @@ void closeMostFDs(const std::set<int> & exceptions)
#endif
int maxFD = 0;
#if HAVE_SYSCONF
maxFD = sysconf(_SC_OPEN_MAX);
#endif
for (int fd = 0; fd < maxFD; ++fd)
if (!exceptions.count(fd))
close(fd); /* ignore result */
}
void closeOnExec(int fd)
void unix::closeOnExec(int fd)
{
int prev;
if ((prev = fcntl(fd, F_GETFD, 0)) == -1 ||

View file

@ -0,0 +1,17 @@
sources += files(
'environment-variables.cc',
'file-descriptor.cc',
'file-path.cc',
'file-system.cc',
'muxable-pipe.cc',
'processes.cc',
'signals.cc',
'users.cc',
)
include_dirs += include_directories('.')
headers += files(
'monitor-fd.hh',
'signals-impl.hh',
)

View file

@ -122,7 +122,7 @@ void Pipe::create()
#if _WIN32_WINNT >= 0x0600
std::wstring handleToFileName(HANDLE handle) {
std::wstring windows::handleToFileName(HANDLE handle) {
std::vector<wchar_t> buf(0x100);
DWORD dw = GetFinalPathNameByHandleW(handle, buf.data(), buf.size(), FILE_NAME_OPENED);
if (dw == 0) {
@ -141,7 +141,7 @@ std::wstring handleToFileName(HANDLE handle) {
}
Path handleToPath(HANDLE handle) {
Path windows::handleToPath(HANDLE handle) {
return os_string_to_string(handleToFileName(handle));
}

View file

@ -0,0 +1,19 @@
sources += files(
'environment-variables.cc',
'file-descriptor.cc',
'file-path.cc',
'file-system.cc',
'muxable-pipe.cc',
'processes.cc',
'users.cc',
'windows-async-pipe.cc',
'windows-error.cc',
)
include_dirs += include_directories('.')
headers += files(
'signals-impl.hh',
'windows-async-pipe.hh',
'windows-error.hh',
)

View file

@ -1,9 +1,15 @@
#include "current-process.hh"
#include "environment-variables.hh"
#include "error.hh"
#include "file-descriptor.hh"
#include "file-path.hh"
#include "signals.hh"
#include "processes.hh"
#include "finally.hh"
#include "serialise.hh"
#include "file-system.hh"
#include "util.hh"
#include "windows-error.hh"
#include <cerrno>
#include <cstdlib>
@ -16,25 +22,347 @@
#include <sys/types.h>
#include <unistd.h>
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
namespace nix {
std::string runProgram(Path program, bool lookupPath, const Strings & args,
const std::optional<std::string> & input, bool isInteractive)
using namespace nix::windows;
Pid::Pid() {}
Pid::Pid(AutoCloseFD pid)
: pid(std::move(pid))
{
throw UnimplementedError("Cannot shell out to git on Windows yet");
}
Pid::~Pid()
{
if (pid.get() != INVALID_DESCRIPTOR)
kill();
}
void Pid::operator=(AutoCloseFD pid)
{
if (this->pid.get() != INVALID_DESCRIPTOR && this->pid.get() != pid.get())
kill();
this->pid = std::move(pid);
}
// TODO: Implement (not needed for process spawning yet)
int Pid::kill()
{
assert(pid.get() != INVALID_DESCRIPTOR);
debug("killing process %1%", pid.get());
throw UnimplementedError("Pid::kill unimplemented");
}
int Pid::wait()
{
// https://github.com/nix-windows/nix/blob/windows-meson/src/libutil/util.cc#L1938
assert(pid.get() != INVALID_DESCRIPTOR);
DWORD status = WaitForSingleObject(pid.get(), INFINITE);
if (status != WAIT_OBJECT_0) {
debug("WaitForSingleObject returned %1%", status);
}
DWORD exitCode = 0;
if (GetExitCodeProcess(pid.get(), &exitCode) == FALSE) {
debug("GetExitCodeProcess failed on pid %1%", pid.get());
}
pid.close();
return exitCode;
}
// TODO: Merge this with Unix's runProgram since it's identical logic.
std::string runProgram(
Path program, bool lookupPath, const Strings & args, const std::optional<std::string> & input, bool isInteractive)
{
auto res = runProgram(RunOptions{
.program = program, .lookupPath = lookupPath, .args = args, .input = input, .isInteractive = isInteractive});
if (!statusOk(res.first))
throw ExecError(res.first, "program '%1%' %2%", program, statusToString(res.first));
return res.second;
}
std::optional<Path> getProgramInterpreter(const Path & program)
{
// These extensions are automatically handled by Windows and don't require an interpreter.
static constexpr const char * exts[] = {".exe", ".cmd", ".bat"};
for (const auto ext : exts) {
if (hasSuffix(program, ext)) {
return {};
}
}
// TODO: Open file and read the shebang
throw UnimplementedError("getProgramInterpreter unimplemented");
}
// TODO: Not sure if this is needed in the unix version but it might be useful as a member func
void setFDInheritable(AutoCloseFD & fd, bool inherit)
{
if (fd.get() != INVALID_DESCRIPTOR) {
if (!SetHandleInformation(fd.get(), HANDLE_FLAG_INHERIT, inherit ? HANDLE_FLAG_INHERIT : 0)) {
throw WinError("Couldn't disable inheriting of handle");
}
}
}
AutoCloseFD nullFD()
{
// Create null handle to discard reads / writes
// https://stackoverflow.com/a/25609668
// https://github.com/nix-windows/nix/blob/windows-meson/src/libutil/util.cc#L2228
AutoCloseFD nul = CreateFileW(
L"NUL",
GENERIC_READ | GENERIC_WRITE,
// We don't care who reads / writes / deletes this file since it's NUL anyways
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
NULL,
OPEN_EXISTING,
0,
NULL);
if (!nul.get()) {
throw WinError("Couldn't open NUL device");
}
// Let this handle be inheritable by child processes
setFDInheritable(nul, true);
return nul;
}
// Adapted from
// https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/
std::string windowsEscape(const std::string & str, bool cmd)
{
// TODO: This doesn't handle cmd.exe escaping.
if (cmd) {
throw UnimplementedError("cmd.exe escaping is not implemented");
}
if (str.find_first_of(" \t\n\v\"") == str.npos && !str.empty()) {
// No need to escape this one, the nonempty contents don't have a special character
return str;
}
std::string buffer;
// Add the opening quote
buffer += '"';
for (auto iter = str.begin();; ++iter) {
size_t backslashes = 0;
while (iter != str.end() && *iter == '\\') {
++iter;
++backslashes;
}
// We only escape backslashes if:
// - They come immediately before the closing quote
// - They come immediately before a quote in the middle of the string
// Both of these cases break the escaping if not handled. Otherwise backslashes are fine as-is
if (iter == str.end()) {
// Need to escape each backslash
buffer.append(backslashes * 2, '\\');
// Exit since we've reached the end of the string
break;
} else if (*iter == '"') {
// Need to escape each backslash and the intermediate quote character
buffer.append(backslashes * 2, '\\');
buffer += "\\\"";
} else {
// Don't escape the backslashes since they won't break the delimiter
buffer.append(backslashes, '\\');
buffer += *iter;
}
}
// Add the closing quote
return buffer + '"';
}
Pid spawnProcess(const Path & realProgram, const RunOptions & options, Pipe & out, Pipe & in)
{
// Setup pipes.
if (options.standardOut) {
// Don't inherit the read end of the output pipe
setFDInheritable(out.readSide, false);
} else {
out.writeSide = nullFD();
}
if (options.standardIn) {
// Don't inherit the write end of the input pipe
setFDInheritable(in.writeSide, false);
} else {
in.readSide = nullFD();
}
STARTUPINFOW startInfo = {0};
startInfo.cb = sizeof(startInfo);
startInfo.dwFlags = STARTF_USESTDHANDLES;
startInfo.hStdInput = in.readSide.get();
startInfo.hStdOutput = out.writeSide.get();
startInfo.hStdError = out.writeSide.get();
std::string envline;
// Retain the current processes' environment variables.
for (const auto & envVar : getEnv()) {
envline += (envVar.first + '=' + envVar.second + '\0');
}
// Also add new ones specified in options.
if (options.environment) {
for (const auto & envVar : *options.environment) {
envline += (envVar.first + '=' + envVar.second + '\0');
}
}
std::string cmdline = windowsEscape(realProgram, false);
for (const auto & arg : options.args) {
// TODO: This isn't the right way to escape windows command
// See https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw
cmdline += ' ' + windowsEscape(arg, false);
}
PROCESS_INFORMATION procInfo = {0};
if (CreateProcessW(
// EXE path is provided in the cmdline
NULL,
string_to_os_string(cmdline).data(),
NULL,
NULL,
TRUE,
CREATE_UNICODE_ENVIRONMENT | CREATE_SUSPENDED,
string_to_os_string(envline).data(),
options.chdir.has_value() ? string_to_os_string(*options.chdir).data() : NULL,
&startInfo,
&procInfo)
== 0) {
throw WinError("CreateProcessW failed (%1%)", cmdline);
}
// Convert these to use RAII
AutoCloseFD process = procInfo.hProcess;
AutoCloseFD thread = procInfo.hThread;
// Add current process and child to job object so child terminates when parent terminates
// TODO: This spawns one job per child process. We can probably keep this as a global, and
// add children a single job so we don't use so many jobs at once.
Descriptor job = CreateJobObjectW(NULL, NULL);
if (job == NULL) {
TerminateProcess(procInfo.hProcess, 0);
throw WinError("Couldn't create job object for child process");
}
if (AssignProcessToJobObject(job, procInfo.hProcess) == FALSE) {
TerminateProcess(procInfo.hProcess, 0);
throw WinError("Couldn't assign child process to job object");
}
if (ResumeThread(procInfo.hThread) == (DWORD) -1) {
TerminateProcess(procInfo.hProcess, 0);
throw WinError("Couldn't resume child process thread");
}
return process;
}
// TODO: Merge this with Unix's runProgram since it's identical logic.
// Output = error code + "standard out" output stream
std::pair<int, std::string> runProgram(RunOptions && options)
{
throw UnimplementedError("Cannot shell out to git on Windows yet");
}
StringSink sink;
options.standardOut = &sink;
int status = 0;
try {
runProgram2(options);
} catch (ExecError & e) {
status = e.status;
}
return {status, std::move(sink.s)};
}
void runProgram2(const RunOptions & options)
{
throw UnimplementedError("Cannot shell out to git on Windows yet");
checkInterrupt();
assert(!(options.standardIn && options.input));
std::unique_ptr<Source> source_;
Source * source = options.standardIn;
if (options.input) {
source_ = std::make_unique<StringSource>(*options.input);
source = source_.get();
}
/* Create a pipe. */
Pipe out, in;
// TODO: I copied this from unix but this is handled again in spawnProcess, so might be weird to split it up like
// this
if (options.standardOut)
out.create();
if (source)
in.create();
Path realProgram = options.program;
// TODO: Implement shebang / program interpreter lookup on Windows
auto interpreter = getProgramInterpreter(realProgram);
std::optional<Finally<std::function<void()>>> resumeLoggerDefer;
if (options.isInteractive) {
logger->pause();
resumeLoggerDefer.emplace([]() { logger->resume(); });
}
Pid pid = spawnProcess(interpreter.has_value() ? *interpreter : realProgram, options, out, in);
// TODO: This is identical to unix, deduplicate?
out.writeSide.close();
std::thread writerThread;
std::promise<void> promise;
Finally doJoin([&] {
if (writerThread.joinable())
writerThread.join();
});
if (source) {
in.readSide.close();
writerThread = std::thread([&] {
try {
std::vector<char> buf(8 * 1024);
while (true) {
size_t n;
try {
n = source->read(buf.data(), buf.size());
} catch (EndOfFile &) {
break;
}
writeFull(in.writeSide.get(), {buf.data(), n});
}
promise.set_value();
} catch (...) {
promise.set_exception(std::current_exception());
}
in.writeSide.close();
});
}
if (options.standardOut)
drainFD(out.readSide.get(), *options.standardOut);
/* Wait for the child to finish. */
int status = pid.wait();
/* Wait for the writer thread to finish. */
if (source)
promise.get_future().get();
if (status)
throw ExecError(status, "program '%1%' %2%", options.program, statusToString(status));
}
std::string statusToString(int status)
@ -45,10 +373,8 @@ std::string statusToString(int status)
return "succeeded";
}
bool statusOk(int status)
{
return status == 0;
}
}