forked from lix-project/lix
Put canonicaliseTimestampAndPermissions
in its own header/file
It is not inherently tied to `LocalStore`, it could probably even go in `libnixutil`. Functions not attached to `LocalStore` should not be declared in `local-store.hh`. I am moving it to facilitate experimenting for #9344. If canonicalisation should be done client-side in client-side builds, there wouldn't be a `LocalStore` at all so having to include that header to get this freestanding function is cumbersome and wrong. Perhaps canonicalisation should still be done server-side for security reasons --- I don't mean to make that judgement call now --- but even if so, this freestanding function still isn't connected to `LocalStore` so while less urgent it is still better to move out of this header.
This commit is contained in:
parent
9cd69e1c39
commit
f880469173
|
@ -19,6 +19,7 @@
|
||||||
#include "namespaces.hh"
|
#include "namespaces.hh"
|
||||||
#include "child.hh"
|
#include "child.hh"
|
||||||
#include "unix-domain-socket.hh"
|
#include "unix-domain-socket.hh"
|
||||||
|
#include "posix-fs-canonicalise.hh"
|
||||||
|
|
||||||
#include <regex>
|
#include <regex>
|
||||||
#include <queue>
|
#include <queue>
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
#include "finally.hh"
|
#include "finally.hh"
|
||||||
#include "compression.hh"
|
#include "compression.hh"
|
||||||
#include "signals.hh"
|
#include "signals.hh"
|
||||||
|
#include "posix-fs-canonicalise.hh"
|
||||||
|
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
@ -581,164 +582,6 @@ void LocalStore::makeStoreWritable()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const time_t mtimeStore = 1; /* 1 second into the epoch */
|
|
||||||
|
|
||||||
|
|
||||||
static void canonicaliseTimestampAndPermissions(const Path & path, const struct stat & st)
|
|
||||||
{
|
|
||||||
if (!S_ISLNK(st.st_mode)) {
|
|
||||||
|
|
||||||
/* Mask out all type related bits. */
|
|
||||||
mode_t mode = st.st_mode & ~S_IFMT;
|
|
||||||
|
|
||||||
if (mode != 0444 && mode != 0555) {
|
|
||||||
mode = (st.st_mode & S_IFMT)
|
|
||||||
| 0444
|
|
||||||
| (st.st_mode & S_IXUSR ? 0111 : 0);
|
|
||||||
if (chmod(path.c_str(), mode) == -1)
|
|
||||||
throw SysError("changing mode of '%1%' to %2$o", path, mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if (st.st_mtime != mtimeStore) {
|
|
||||||
struct timeval times[2];
|
|
||||||
times[0].tv_sec = st.st_atime;
|
|
||||||
times[0].tv_usec = 0;
|
|
||||||
times[1].tv_sec = mtimeStore;
|
|
||||||
times[1].tv_usec = 0;
|
|
||||||
#if HAVE_LUTIMES
|
|
||||||
if (lutimes(path.c_str(), times) == -1)
|
|
||||||
if (errno != ENOSYS ||
|
|
||||||
(!S_ISLNK(st.st_mode) && utimes(path.c_str(), times) == -1))
|
|
||||||
#else
|
|
||||||
if (!S_ISLNK(st.st_mode) && utimes(path.c_str(), times) == -1)
|
|
||||||
#endif
|
|
||||||
throw SysError("changing modification time of '%1%'", path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void canonicaliseTimestampAndPermissions(const Path & path)
|
|
||||||
{
|
|
||||||
canonicaliseTimestampAndPermissions(path, lstat(path));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static void canonicalisePathMetaData_(
|
|
||||||
const Path & path,
|
|
||||||
std::optional<std::pair<uid_t, uid_t>> uidRange,
|
|
||||||
InodesSeen & inodesSeen)
|
|
||||||
{
|
|
||||||
checkInterrupt();
|
|
||||||
|
|
||||||
#if __APPLE__
|
|
||||||
/* Remove flags, in particular UF_IMMUTABLE which would prevent
|
|
||||||
the file from being garbage-collected. FIXME: Use
|
|
||||||
setattrlist() to remove other attributes as well. */
|
|
||||||
if (lchflags(path.c_str(), 0)) {
|
|
||||||
if (errno != ENOTSUP)
|
|
||||||
throw SysError("clearing flags of path '%1%'", path);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
auto st = lstat(path);
|
|
||||||
|
|
||||||
/* Really make sure that the path is of a supported type. */
|
|
||||||
if (!(S_ISREG(st.st_mode) || S_ISDIR(st.st_mode) || S_ISLNK(st.st_mode)))
|
|
||||||
throw Error("file '%1%' has an unsupported type", path);
|
|
||||||
|
|
||||||
#if __linux__
|
|
||||||
/* Remove extended attributes / ACLs. */
|
|
||||||
ssize_t eaSize = llistxattr(path.c_str(), nullptr, 0);
|
|
||||||
|
|
||||||
if (eaSize < 0) {
|
|
||||||
if (errno != ENOTSUP && errno != ENODATA)
|
|
||||||
throw SysError("querying extended attributes of '%s'", path);
|
|
||||||
} else if (eaSize > 0) {
|
|
||||||
std::vector<char> eaBuf(eaSize);
|
|
||||||
|
|
||||||
if ((eaSize = llistxattr(path.c_str(), eaBuf.data(), eaBuf.size())) < 0)
|
|
||||||
throw SysError("querying extended attributes of '%s'", path);
|
|
||||||
|
|
||||||
for (auto & eaName: tokenizeString<Strings>(std::string(eaBuf.data(), eaSize), std::string("\000", 1))) {
|
|
||||||
if (settings.ignoredAcls.get().count(eaName)) continue;
|
|
||||||
if (lremovexattr(path.c_str(), eaName.c_str()) == -1)
|
|
||||||
throw SysError("removing extended attribute '%s' from '%s'", eaName, path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/* Fail if the file is not owned by the build user. This prevents
|
|
||||||
us from messing up the ownership/permissions of files
|
|
||||||
hard-linked into the output (e.g. "ln /etc/shadow $out/foo").
|
|
||||||
However, ignore files that we chown'ed ourselves previously to
|
|
||||||
ensure that we don't fail on hard links within the same build
|
|
||||||
(i.e. "touch $out/foo; ln $out/foo $out/bar"). */
|
|
||||||
if (uidRange && (st.st_uid < uidRange->first || st.st_uid > uidRange->second)) {
|
|
||||||
if (S_ISDIR(st.st_mode) || !inodesSeen.count(Inode(st.st_dev, st.st_ino)))
|
|
||||||
throw BuildError("invalid ownership on file '%1%'", path);
|
|
||||||
mode_t mode = st.st_mode & ~S_IFMT;
|
|
||||||
assert(S_ISLNK(st.st_mode) || (st.st_uid == geteuid() && (mode == 0444 || mode == 0555) && st.st_mtime == mtimeStore));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
inodesSeen.insert(Inode(st.st_dev, st.st_ino));
|
|
||||||
|
|
||||||
canonicaliseTimestampAndPermissions(path, st);
|
|
||||||
|
|
||||||
/* Change ownership to the current uid. If it's a symlink, use
|
|
||||||
lchown if available, otherwise don't bother. Wrong ownership
|
|
||||||
of a symlink doesn't matter, since the owning user can't change
|
|
||||||
the symlink and can't delete it because the directory is not
|
|
||||||
writable. The only exception is top-level paths in the Nix
|
|
||||||
store (since that directory is group-writable for the Nix build
|
|
||||||
users group); we check for this case below. */
|
|
||||||
if (st.st_uid != geteuid()) {
|
|
||||||
#if HAVE_LCHOWN
|
|
||||||
if (lchown(path.c_str(), geteuid(), getegid()) == -1)
|
|
||||||
#else
|
|
||||||
if (!S_ISLNK(st.st_mode) &&
|
|
||||||
chown(path.c_str(), geteuid(), getegid()) == -1)
|
|
||||||
#endif
|
|
||||||
throw SysError("changing owner of '%1%' to %2%",
|
|
||||||
path, geteuid());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (S_ISDIR(st.st_mode)) {
|
|
||||||
DirEntries entries = readDirectory(path);
|
|
||||||
for (auto & i : entries)
|
|
||||||
canonicalisePathMetaData_(path + "/" + i.name, uidRange, inodesSeen);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void canonicalisePathMetaData(
|
|
||||||
const Path & path,
|
|
||||||
std::optional<std::pair<uid_t, uid_t>> uidRange,
|
|
||||||
InodesSeen & inodesSeen)
|
|
||||||
{
|
|
||||||
canonicalisePathMetaData_(path, uidRange, inodesSeen);
|
|
||||||
|
|
||||||
/* On platforms that don't have lchown(), the top-level path can't
|
|
||||||
be a symlink, since we can't change its ownership. */
|
|
||||||
auto st = lstat(path);
|
|
||||||
|
|
||||||
if (st.st_uid != geteuid()) {
|
|
||||||
assert(S_ISLNK(st.st_mode));
|
|
||||||
throw Error("wrong ownership of top-level store path '%1%'", path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void canonicalisePathMetaData(const Path & path,
|
|
||||||
std::optional<std::pair<uid_t, uid_t>> uidRange)
|
|
||||||
{
|
|
||||||
InodesSeen inodesSeen;
|
|
||||||
canonicalisePathMetaData(path, uidRange, inodesSeen);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void LocalStore::registerDrvOutput(const Realisation & info, CheckSigsFlag checkSigs)
|
void LocalStore::registerDrvOutput(const Realisation & info, CheckSigsFlag checkSigs)
|
||||||
{
|
{
|
||||||
experimentalFeatureSettings.require(Xp::CaDerivations);
|
experimentalFeatureSettings.require(Xp::CaDerivations);
|
||||||
|
|
|
@ -371,38 +371,4 @@ private:
|
||||||
friend struct DerivationGoal;
|
friend struct DerivationGoal;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
typedef std::pair<dev_t, ino_t> Inode;
|
|
||||||
typedef std::set<Inode> InodesSeen;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* "Fix", or canonicalise, the meta-data of the files in a store path
|
|
||||||
* after it has been built. In particular:
|
|
||||||
*
|
|
||||||
* - the last modification date on each file is set to 1 (i.e.,
|
|
||||||
* 00:00:01 1/1/1970 UTC)
|
|
||||||
*
|
|
||||||
* - the permissions are set of 444 or 555 (i.e., read-only with or
|
|
||||||
* without execute permission; setuid bits etc. are cleared)
|
|
||||||
*
|
|
||||||
* - the owner and group are set to the Nix user and group, if we're
|
|
||||||
* running as root.
|
|
||||||
*
|
|
||||||
* If uidRange is not empty, this function will throw an error if it
|
|
||||||
* encounters files owned by a user outside of the closed interval
|
|
||||||
* [uidRange->first, uidRange->second].
|
|
||||||
*/
|
|
||||||
void canonicalisePathMetaData(
|
|
||||||
const Path & path,
|
|
||||||
std::optional<std::pair<uid_t, uid_t>> uidRange,
|
|
||||||
InodesSeen & inodesSeen);
|
|
||||||
void canonicalisePathMetaData(
|
|
||||||
const Path & path,
|
|
||||||
std::optional<std::pair<uid_t, uid_t>> uidRange);
|
|
||||||
|
|
||||||
void canonicaliseTimestampAndPermissions(const Path & path);
|
|
||||||
|
|
||||||
MakeError(PathInUse, Error);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
#include "local-store.hh"
|
#include "local-store.hh"
|
||||||
#include "globals.hh"
|
#include "globals.hh"
|
||||||
#include "signals.hh"
|
#include "signals.hh"
|
||||||
|
#include "posix-fs-canonicalise.hh"
|
||||||
|
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
169
src/libstore/posix-fs-canonicalise.cc
Normal file
169
src/libstore/posix-fs-canonicalise.cc
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
#include <sys/xattr.h>
|
||||||
|
|
||||||
|
#include "posix-fs-canonicalise.hh"
|
||||||
|
#include "file-system.hh"
|
||||||
|
#include "signals.hh"
|
||||||
|
#include "util.hh"
|
||||||
|
#include "globals.hh"
|
||||||
|
#include "store-api.hh"
|
||||||
|
|
||||||
|
namespace nix {
|
||||||
|
|
||||||
|
const time_t mtimeStore = 1; /* 1 second into the epoch */
|
||||||
|
|
||||||
|
|
||||||
|
static void canonicaliseTimestampAndPermissions(const Path & path, const struct stat & st)
|
||||||
|
{
|
||||||
|
if (!S_ISLNK(st.st_mode)) {
|
||||||
|
|
||||||
|
/* Mask out all type related bits. */
|
||||||
|
mode_t mode = st.st_mode & ~S_IFMT;
|
||||||
|
|
||||||
|
if (mode != 0444 && mode != 0555) {
|
||||||
|
mode = (st.st_mode & S_IFMT)
|
||||||
|
| 0444
|
||||||
|
| (st.st_mode & S_IXUSR ? 0111 : 0);
|
||||||
|
if (chmod(path.c_str(), mode) == -1)
|
||||||
|
throw SysError("changing mode of '%1%' to %2$o", path, mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (st.st_mtime != mtimeStore) {
|
||||||
|
struct timeval times[2];
|
||||||
|
times[0].tv_sec = st.st_atime;
|
||||||
|
times[0].tv_usec = 0;
|
||||||
|
times[1].tv_sec = mtimeStore;
|
||||||
|
times[1].tv_usec = 0;
|
||||||
|
#if HAVE_LUTIMES
|
||||||
|
if (lutimes(path.c_str(), times) == -1)
|
||||||
|
if (errno != ENOSYS ||
|
||||||
|
(!S_ISLNK(st.st_mode) && utimes(path.c_str(), times) == -1))
|
||||||
|
#else
|
||||||
|
if (!S_ISLNK(st.st_mode) && utimes(path.c_str(), times) == -1)
|
||||||
|
#endif
|
||||||
|
throw SysError("changing modification time of '%1%'", path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void canonicaliseTimestampAndPermissions(const Path & path)
|
||||||
|
{
|
||||||
|
canonicaliseTimestampAndPermissions(path, lstat(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void canonicalisePathMetaData_(
|
||||||
|
const Path & path,
|
||||||
|
std::optional<std::pair<uid_t, uid_t>> uidRange,
|
||||||
|
InodesSeen & inodesSeen)
|
||||||
|
{
|
||||||
|
checkInterrupt();
|
||||||
|
|
||||||
|
#if __APPLE__
|
||||||
|
/* Remove flags, in particular UF_IMMUTABLE which would prevent
|
||||||
|
the file from being garbage-collected. FIXME: Use
|
||||||
|
setattrlist() to remove other attributes as well. */
|
||||||
|
if (lchflags(path.c_str(), 0)) {
|
||||||
|
if (errno != ENOTSUP)
|
||||||
|
throw SysError("clearing flags of path '%1%'", path);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
auto st = lstat(path);
|
||||||
|
|
||||||
|
/* Really make sure that the path is of a supported type. */
|
||||||
|
if (!(S_ISREG(st.st_mode) || S_ISDIR(st.st_mode) || S_ISLNK(st.st_mode)))
|
||||||
|
throw Error("file '%1%' has an unsupported type", path);
|
||||||
|
|
||||||
|
#if __linux__
|
||||||
|
/* Remove extended attributes / ACLs. */
|
||||||
|
ssize_t eaSize = llistxattr(path.c_str(), nullptr, 0);
|
||||||
|
|
||||||
|
if (eaSize < 0) {
|
||||||
|
if (errno != ENOTSUP && errno != ENODATA)
|
||||||
|
throw SysError("querying extended attributes of '%s'", path);
|
||||||
|
} else if (eaSize > 0) {
|
||||||
|
std::vector<char> eaBuf(eaSize);
|
||||||
|
|
||||||
|
if ((eaSize = llistxattr(path.c_str(), eaBuf.data(), eaBuf.size())) < 0)
|
||||||
|
throw SysError("querying extended attributes of '%s'", path);
|
||||||
|
|
||||||
|
for (auto & eaName: tokenizeString<Strings>(std::string(eaBuf.data(), eaSize), std::string("\000", 1))) {
|
||||||
|
if (settings.ignoredAcls.get().count(eaName)) continue;
|
||||||
|
if (lremovexattr(path.c_str(), eaName.c_str()) == -1)
|
||||||
|
throw SysError("removing extended attribute '%s' from '%s'", eaName, path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Fail if the file is not owned by the build user. This prevents
|
||||||
|
us from messing up the ownership/permissions of files
|
||||||
|
hard-linked into the output (e.g. "ln /etc/shadow $out/foo").
|
||||||
|
However, ignore files that we chown'ed ourselves previously to
|
||||||
|
ensure that we don't fail on hard links within the same build
|
||||||
|
(i.e. "touch $out/foo; ln $out/foo $out/bar"). */
|
||||||
|
if (uidRange && (st.st_uid < uidRange->first || st.st_uid > uidRange->second)) {
|
||||||
|
if (S_ISDIR(st.st_mode) || !inodesSeen.count(Inode(st.st_dev, st.st_ino)))
|
||||||
|
throw BuildError("invalid ownership on file '%1%'", path);
|
||||||
|
mode_t mode = st.st_mode & ~S_IFMT;
|
||||||
|
assert(S_ISLNK(st.st_mode) || (st.st_uid == geteuid() && (mode == 0444 || mode == 0555) && st.st_mtime == mtimeStore));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
inodesSeen.insert(Inode(st.st_dev, st.st_ino));
|
||||||
|
|
||||||
|
canonicaliseTimestampAndPermissions(path, st);
|
||||||
|
|
||||||
|
/* Change ownership to the current uid. If it's a symlink, use
|
||||||
|
lchown if available, otherwise don't bother. Wrong ownership
|
||||||
|
of a symlink doesn't matter, since the owning user can't change
|
||||||
|
the symlink and can't delete it because the directory is not
|
||||||
|
writable. The only exception is top-level paths in the Nix
|
||||||
|
store (since that directory is group-writable for the Nix build
|
||||||
|
users group); we check for this case below. */
|
||||||
|
if (st.st_uid != geteuid()) {
|
||||||
|
#if HAVE_LCHOWN
|
||||||
|
if (lchown(path.c_str(), geteuid(), getegid()) == -1)
|
||||||
|
#else
|
||||||
|
if (!S_ISLNK(st.st_mode) &&
|
||||||
|
chown(path.c_str(), geteuid(), getegid()) == -1)
|
||||||
|
#endif
|
||||||
|
throw SysError("changing owner of '%1%' to %2%",
|
||||||
|
path, geteuid());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (S_ISDIR(st.st_mode)) {
|
||||||
|
DirEntries entries = readDirectory(path);
|
||||||
|
for (auto & i : entries)
|
||||||
|
canonicalisePathMetaData_(path + "/" + i.name, uidRange, inodesSeen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void canonicalisePathMetaData(
|
||||||
|
const Path & path,
|
||||||
|
std::optional<std::pair<uid_t, uid_t>> uidRange,
|
||||||
|
InodesSeen & inodesSeen)
|
||||||
|
{
|
||||||
|
canonicalisePathMetaData_(path, uidRange, inodesSeen);
|
||||||
|
|
||||||
|
/* On platforms that don't have lchown(), the top-level path can't
|
||||||
|
be a symlink, since we can't change its ownership. */
|
||||||
|
auto st = lstat(path);
|
||||||
|
|
||||||
|
if (st.st_uid != geteuid()) {
|
||||||
|
assert(S_ISLNK(st.st_mode));
|
||||||
|
throw Error("wrong ownership of top-level store path '%1%'", path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void canonicalisePathMetaData(const Path & path,
|
||||||
|
std::optional<std::pair<uid_t, uid_t>> uidRange)
|
||||||
|
{
|
||||||
|
InodesSeen inodesSeen;
|
||||||
|
canonicalisePathMetaData(path, uidRange, inodesSeen);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
45
src/libstore/posix-fs-canonicalise.hh
Normal file
45
src/libstore/posix-fs-canonicalise.hh
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
#pragma once
|
||||||
|
///@file
|
||||||
|
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/time.h>
|
||||||
|
|
||||||
|
#include "types.hh"
|
||||||
|
#include "error.hh"
|
||||||
|
|
||||||
|
namespace nix {
|
||||||
|
|
||||||
|
typedef std::pair<dev_t, ino_t> Inode;
|
||||||
|
typedef std::set<Inode> InodesSeen;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "Fix", or canonicalise, the meta-data of the files in a store path
|
||||||
|
* after it has been built. In particular:
|
||||||
|
*
|
||||||
|
* - the last modification date on each file is set to 1 (i.e.,
|
||||||
|
* 00:00:01 1/1/1970 UTC)
|
||||||
|
*
|
||||||
|
* - the permissions are set of 444 or 555 (i.e., read-only with or
|
||||||
|
* without execute permission; setuid bits etc. are cleared)
|
||||||
|
*
|
||||||
|
* - the owner and group are set to the Nix user and group, if we're
|
||||||
|
* running as root.
|
||||||
|
*
|
||||||
|
* If uidRange is not empty, this function will throw an error if it
|
||||||
|
* encounters files owned by a user outside of the closed interval
|
||||||
|
* [uidRange->first, uidRange->second].
|
||||||
|
*/
|
||||||
|
void canonicalisePathMetaData(
|
||||||
|
const Path & path,
|
||||||
|
std::optional<std::pair<uid_t, uid_t>> uidRange,
|
||||||
|
InodesSeen & inodesSeen);
|
||||||
|
void canonicalisePathMetaData(
|
||||||
|
const Path & path,
|
||||||
|
std::optional<std::pair<uid_t, uid_t>> uidRange);
|
||||||
|
|
||||||
|
void canonicaliseTimestampAndPermissions(const Path & path);
|
||||||
|
|
||||||
|
MakeError(PathInUse, Error);
|
||||||
|
|
||||||
|
}
|
|
@ -14,6 +14,7 @@
|
||||||
#include "graphml.hh"
|
#include "graphml.hh"
|
||||||
#include "legacy.hh"
|
#include "legacy.hh"
|
||||||
#include "path-with-outputs.hh"
|
#include "path-with-outputs.hh"
|
||||||
|
#include "posix-fs-canonicalise.hh"
|
||||||
|
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
Loading…
Reference in a new issue