mirror of
https://github.com/NixOS/nix.git
synced 2025-11-09 03:56:01 +01:00
Merge pull request #14013 from xokdvium/revert-13938
Revert "Merge pull request #13938 from NixOS/import-thunk"
This commit is contained in:
commit
9d8c6a6646
9 changed files with 91 additions and 138 deletions
|
|
@ -38,7 +38,6 @@
|
||||||
|
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
#include <boost/container/small_vector.hpp>
|
#include <boost/container/small_vector.hpp>
|
||||||
#include <boost/unordered/concurrent_flat_map.hpp>
|
|
||||||
|
|
||||||
#include "nix/util/strings-inline.hh"
|
#include "nix/util/strings-inline.hh"
|
||||||
|
|
||||||
|
|
@ -265,9 +264,6 @@ EvalState::EvalState(
|
||||||
, debugRepl(nullptr)
|
, debugRepl(nullptr)
|
||||||
, debugStop(false)
|
, debugStop(false)
|
||||||
, trylevel(0)
|
, trylevel(0)
|
||||||
, srcToStore(make_ref<decltype(srcToStore)::element_type>())
|
|
||||||
, importResolutionCache(make_ref<decltype(importResolutionCache)::element_type>())
|
|
||||||
, fileEvalCache(make_ref<decltype(fileEvalCache)::element_type>())
|
|
||||||
, regexCache(makeRegexCache())
|
, regexCache(makeRegexCache())
|
||||||
#if NIX_USE_BOEHMGC
|
#if NIX_USE_BOEHMGC
|
||||||
, valueAllocCache(std::allocate_shared<void *>(traceable_allocator<void *>(), nullptr))
|
, valueAllocCache(std::allocate_shared<void *>(traceable_allocator<void *>(), nullptr))
|
||||||
|
|
@ -1030,85 +1026,63 @@ Value * ExprPath::maybeThunk(EvalState & state, Env & env)
|
||||||
return &v;
|
return &v;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A helper `Expr` class to lets us parse and evaluate Nix expressions
|
|
||||||
* from a thunk, ensuring that every file is parsed/evaluated only
|
|
||||||
* once (via the thunk stored in `EvalState::fileEvalCache`).
|
|
||||||
*/
|
|
||||||
struct ExprParseFile : Expr
|
|
||||||
{
|
|
||||||
SourcePath & path;
|
|
||||||
bool mustBeTrivial;
|
|
||||||
|
|
||||||
ExprParseFile(SourcePath & path, bool mustBeTrivial)
|
|
||||||
: path(path)
|
|
||||||
, mustBeTrivial(mustBeTrivial)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
void eval(EvalState & state, Env & env, Value & v) override
|
|
||||||
{
|
|
||||||
printTalkative("evaluating file '%s'", path);
|
|
||||||
|
|
||||||
auto e = state.parseExprFromFile(path);
|
|
||||||
|
|
||||||
try {
|
|
||||||
auto dts =
|
|
||||||
state.debugRepl
|
|
||||||
? makeDebugTraceStacker(
|
|
||||||
state, *e, state.baseEnv, e->getPos(), "while evaluating the file '%s':", path.to_string())
|
|
||||||
: nullptr;
|
|
||||||
|
|
||||||
// Enforce that 'flake.nix' is a direct attrset, not a
|
|
||||||
// computation.
|
|
||||||
if (mustBeTrivial && !(dynamic_cast<ExprAttrs *>(e)))
|
|
||||||
state.error<EvalError>("file '%s' must be an attribute set", path).debugThrow();
|
|
||||||
|
|
||||||
state.eval(e, v);
|
|
||||||
} catch (Error & e) {
|
|
||||||
state.addErrorTrace(e, "while evaluating the file '%s':", path.to_string());
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
void EvalState::evalFile(const SourcePath & path, Value & v, bool mustBeTrivial)
|
void EvalState::evalFile(const SourcePath & path, Value & v, bool mustBeTrivial)
|
||||||
{
|
{
|
||||||
auto resolvedPath = getConcurrent(*importResolutionCache, path);
|
FileEvalCache::iterator i;
|
||||||
|
if ((i = fileEvalCache.find(path)) != fileEvalCache.end()) {
|
||||||
if (!resolvedPath) {
|
v = i->second;
|
||||||
resolvedPath = resolveExprPath(path);
|
|
||||||
importResolutionCache->emplace(path, *resolvedPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (auto v2 = getConcurrent(*fileEvalCache, *resolvedPath)) {
|
|
||||||
forceValue(**v2, noPos);
|
|
||||||
v = **v2;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Value * vExpr;
|
auto resolvedPath = resolveExprPath(path);
|
||||||
ExprParseFile expr{*resolvedPath, mustBeTrivial};
|
if ((i = fileEvalCache.find(resolvedPath)) != fileEvalCache.end()) {
|
||||||
|
v = i->second;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
fileEvalCache->try_emplace_and_cvisit(
|
printTalkative("evaluating file '%1%'", resolvedPath);
|
||||||
*resolvedPath,
|
Expr * e = nullptr;
|
||||||
nullptr,
|
|
||||||
[&](auto & i) {
|
|
||||||
vExpr = allocValue();
|
|
||||||
vExpr->mkThunk(&baseEnv, &expr);
|
|
||||||
i.second = vExpr;
|
|
||||||
},
|
|
||||||
[&](auto & i) { vExpr = i.second; });
|
|
||||||
|
|
||||||
forceValue(*vExpr, noPos);
|
auto j = fileParseCache.find(resolvedPath);
|
||||||
|
if (j != fileParseCache.end())
|
||||||
|
e = j->second;
|
||||||
|
|
||||||
v = *vExpr;
|
if (!e)
|
||||||
|
e = parseExprFromFile(resolvedPath);
|
||||||
|
|
||||||
|
fileParseCache.emplace(resolvedPath, e);
|
||||||
|
|
||||||
|
try {
|
||||||
|
auto dts = debugRepl ? makeDebugTraceStacker(
|
||||||
|
*this,
|
||||||
|
*e,
|
||||||
|
this->baseEnv,
|
||||||
|
e->getPos(),
|
||||||
|
"while evaluating the file '%1%':",
|
||||||
|
resolvedPath.to_string())
|
||||||
|
: nullptr;
|
||||||
|
|
||||||
|
// Enforce that 'flake.nix' is a direct attrset, not a
|
||||||
|
// computation.
|
||||||
|
if (mustBeTrivial && !(dynamic_cast<ExprAttrs *>(e)))
|
||||||
|
error<EvalError>("file '%s' must be an attribute set", path).debugThrow();
|
||||||
|
eval(e, v);
|
||||||
|
} catch (Error & e) {
|
||||||
|
addErrorTrace(e, "while evaluating the file '%1%':", resolvedPath.to_string());
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
fileEvalCache.emplace(resolvedPath, v);
|
||||||
|
if (path != resolvedPath)
|
||||||
|
fileEvalCache.emplace(path, v);
|
||||||
}
|
}
|
||||||
|
|
||||||
void EvalState::resetFileCache()
|
void EvalState::resetFileCache()
|
||||||
{
|
{
|
||||||
importResolutionCache->clear();
|
fileEvalCache.clear();
|
||||||
fileEvalCache->clear();
|
fileEvalCache.rehash(0);
|
||||||
|
fileParseCache.clear();
|
||||||
|
fileParseCache.rehash(0);
|
||||||
inputCache->clear();
|
inputCache->clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2427,10 +2401,9 @@ StorePath EvalState::copyPathToStore(NixStringContext & context, const SourcePat
|
||||||
if (nix::isDerivation(path.path.abs()))
|
if (nix::isDerivation(path.path.abs()))
|
||||||
error<EvalError>("file names are not allowed to end in '%1%'", drvExtension).debugThrow();
|
error<EvalError>("file names are not allowed to end in '%1%'", drvExtension).debugThrow();
|
||||||
|
|
||||||
auto dstPathCached = getConcurrent(*srcToStore, path);
|
std::optional<StorePath> dstPath;
|
||||||
|
if (!srcToStore.cvisit(path, [&dstPath](const auto & kv) { dstPath.emplace(kv.second); })) {
|
||||||
auto dstPath = dstPathCached ? *dstPathCached : [&]() {
|
dstPath.emplace(fetchToStore(
|
||||||
auto dstPath = fetchToStore(
|
|
||||||
fetchSettings,
|
fetchSettings,
|
||||||
*store,
|
*store,
|
||||||
path.resolveSymlinks(SymlinkResolution::Ancestors),
|
path.resolveSymlinks(SymlinkResolution::Ancestors),
|
||||||
|
|
@ -2438,15 +2411,14 @@ StorePath EvalState::copyPathToStore(NixStringContext & context, const SourcePat
|
||||||
path.baseName(),
|
path.baseName(),
|
||||||
ContentAddressMethod::Raw::NixArchive,
|
ContentAddressMethod::Raw::NixArchive,
|
||||||
nullptr,
|
nullptr,
|
||||||
repair);
|
repair));
|
||||||
allowPath(dstPath);
|
allowPath(*dstPath);
|
||||||
srcToStore->try_emplace(path, dstPath);
|
srcToStore.try_emplace(path, *dstPath);
|
||||||
printMsg(lvlChatty, "copied source '%1%' -> '%2%'", path, store->printStorePath(dstPath));
|
printMsg(lvlChatty, "copied source '%1%' -> '%2%'", path, store->printStorePath(*dstPath));
|
||||||
return dstPath;
|
}
|
||||||
}();
|
|
||||||
|
|
||||||
context.insert(NixStringContextElem::Opaque{.path = dstPath});
|
context.insert(NixStringContextElem::Opaque{.path = *dstPath});
|
||||||
return dstPath;
|
return *dstPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
SourcePath EvalState::coerceToPath(const PosIdx pos, Value & v, NixStringContext & context, std::string_view errorCtx)
|
SourcePath EvalState::coerceToPath(const PosIdx pos, Value & v, NixStringContext & context, std::string_view errorCtx)
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,8 @@
|
||||||
// For `NIX_USE_BOEHMGC`, and if that's set, `GC_THREADS`
|
// For `NIX_USE_BOEHMGC`, and if that's set, `GC_THREADS`
|
||||||
#include "nix/expr/config.hh"
|
#include "nix/expr/config.hh"
|
||||||
|
|
||||||
|
#include <boost/unordered/concurrent_flat_map.hpp>
|
||||||
#include <boost/unordered/unordered_flat_map.hpp>
|
#include <boost/unordered/unordered_flat_map.hpp>
|
||||||
#include <boost/unordered/concurrent_flat_map_fwd.hpp>
|
|
||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
@ -404,30 +403,37 @@ private:
|
||||||
|
|
||||||
/* Cache for calls to addToStore(); maps source paths to the store
|
/* Cache for calls to addToStore(); maps source paths to the store
|
||||||
paths. */
|
paths. */
|
||||||
ref<boost::concurrent_flat_map<SourcePath, StorePath>> srcToStore;
|
boost::concurrent_flat_map<SourcePath, StorePath, std::hash<SourcePath>> srcToStore;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A cache that maps paths to "resolved" paths for importing Nix
|
* A cache from path names to parse trees.
|
||||||
* expressions, i.e. `/foo` to `/foo/default.nix`.
|
|
||||||
*/
|
*/
|
||||||
ref<boost::concurrent_flat_map<SourcePath, SourcePath>> importResolutionCache;
|
typedef boost::unordered_flat_map<
|
||||||
|
|
||||||
/**
|
|
||||||
* A cache from resolved paths to values.
|
|
||||||
*/
|
|
||||||
ref<boost::concurrent_flat_map<
|
|
||||||
SourcePath,
|
SourcePath,
|
||||||
Value *,
|
Expr *,
|
||||||
std::hash<SourcePath>,
|
std::hash<SourcePath>,
|
||||||
std::equal_to<SourcePath>,
|
std::equal_to<SourcePath>,
|
||||||
traceable_allocator<std::pair<const SourcePath, Value *>>>>
|
traceable_allocator<std::pair<const SourcePath, Expr *>>>
|
||||||
fileEvalCache;
|
FileParseCache;
|
||||||
|
FileParseCache fileParseCache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A cache from path names to values.
|
||||||
|
*/
|
||||||
|
typedef boost::unordered_flat_map<
|
||||||
|
SourcePath,
|
||||||
|
Value,
|
||||||
|
std::hash<SourcePath>,
|
||||||
|
std::equal_to<SourcePath>,
|
||||||
|
traceable_allocator<std::pair<const SourcePath, Value>>>
|
||||||
|
FileEvalCache;
|
||||||
|
FileEvalCache fileEvalCache;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Associate source positions of certain AST nodes with their preceding doc comment, if they have one.
|
* Associate source positions of certain AST nodes with their preceding doc comment, if they have one.
|
||||||
* Grouped by file.
|
* Grouped by file.
|
||||||
*/
|
*/
|
||||||
boost::unordered_flat_map<SourcePath, DocCommentMap> positionToDocComment;
|
boost::unordered_flat_map<SourcePath, DocCommentMap, std::hash<SourcePath>> positionToDocComment;
|
||||||
|
|
||||||
LookupPath lookupPath;
|
LookupPath lookupPath;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,12 +59,12 @@ void FilteringSourceAccessor::checkAccess(const CanonPath & path)
|
||||||
struct AllowListSourceAccessorImpl : AllowListSourceAccessor
|
struct AllowListSourceAccessorImpl : AllowListSourceAccessor
|
||||||
{
|
{
|
||||||
std::set<CanonPath> allowedPrefixes;
|
std::set<CanonPath> allowedPrefixes;
|
||||||
boost::unordered_flat_set<CanonPath> allowedPaths;
|
boost::unordered_flat_set<CanonPath, std::hash<CanonPath>> allowedPaths;
|
||||||
|
|
||||||
AllowListSourceAccessorImpl(
|
AllowListSourceAccessorImpl(
|
||||||
ref<SourceAccessor> next,
|
ref<SourceAccessor> next,
|
||||||
std::set<CanonPath> && allowedPrefixes,
|
std::set<CanonPath> && allowedPrefixes,
|
||||||
boost::unordered_flat_set<CanonPath> && allowedPaths,
|
boost::unordered_flat_set<CanonPath, std::hash<CanonPath>> && allowedPaths,
|
||||||
MakeNotAllowedError && makeNotAllowedError)
|
MakeNotAllowedError && makeNotAllowedError)
|
||||||
: AllowListSourceAccessor(SourcePath(next), std::move(makeNotAllowedError))
|
: AllowListSourceAccessor(SourcePath(next), std::move(makeNotAllowedError))
|
||||||
, allowedPrefixes(std::move(allowedPrefixes))
|
, allowedPrefixes(std::move(allowedPrefixes))
|
||||||
|
|
@ -86,7 +86,7 @@ struct AllowListSourceAccessorImpl : AllowListSourceAccessor
|
||||||
ref<AllowListSourceAccessor> AllowListSourceAccessor::create(
|
ref<AllowListSourceAccessor> AllowListSourceAccessor::create(
|
||||||
ref<SourceAccessor> next,
|
ref<SourceAccessor> next,
|
||||||
std::set<CanonPath> && allowedPrefixes,
|
std::set<CanonPath> && allowedPrefixes,
|
||||||
boost::unordered_flat_set<CanonPath> && allowedPaths,
|
boost::unordered_flat_set<CanonPath, std::hash<CanonPath>> && allowedPaths,
|
||||||
MakeNotAllowedError && makeNotAllowedError)
|
MakeNotAllowedError && makeNotAllowedError)
|
||||||
{
|
{
|
||||||
return make_ref<AllowListSourceAccessorImpl>(
|
return make_ref<AllowListSourceAccessorImpl>(
|
||||||
|
|
|
||||||
|
|
@ -817,7 +817,7 @@ struct GitSourceAccessor : SourceAccessor
|
||||||
return toHash(*git_tree_entry_id(entry));
|
return toHash(*git_tree_entry_id(entry));
|
||||||
}
|
}
|
||||||
|
|
||||||
boost::unordered_flat_map<CanonPath, TreeEntry> lookupCache;
|
boost::unordered_flat_map<CanonPath, TreeEntry, std::hash<CanonPath>> lookupCache;
|
||||||
|
|
||||||
/* Recursively look up 'path' relative to the root. */
|
/* Recursively look up 'path' relative to the root. */
|
||||||
git_tree_entry * lookup(State & state, const CanonPath & path)
|
git_tree_entry * lookup(State & state, const CanonPath & path)
|
||||||
|
|
@ -1254,7 +1254,7 @@ GitRepoImpl::getAccessor(const WorkdirInfo & wd, bool exportIgnore, MakeNotAllow
|
||||||
makeFSSourceAccessor(path),
|
makeFSSourceAccessor(path),
|
||||||
std::set<CanonPath>{wd.files},
|
std::set<CanonPath>{wd.files},
|
||||||
// Always allow access to the root, but not its children.
|
// Always allow access to the root, but not its children.
|
||||||
boost::unordered_flat_set<CanonPath>{CanonPath::root},
|
boost::unordered_flat_set<CanonPath, std::hash<CanonPath>>{CanonPath::root},
|
||||||
std::move(makeNotAllowedError))
|
std::move(makeNotAllowedError))
|
||||||
.cast<SourceAccessor>();
|
.cast<SourceAccessor>();
|
||||||
if (exportIgnore)
|
if (exportIgnore)
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ struct AllowListSourceAccessor : public FilteringSourceAccessor
|
||||||
static ref<AllowListSourceAccessor> create(
|
static ref<AllowListSourceAccessor> create(
|
||||||
ref<SourceAccessor> next,
|
ref<SourceAccessor> next,
|
||||||
std::set<CanonPath> && allowedPrefixes,
|
std::set<CanonPath> && allowedPrefixes,
|
||||||
boost::unordered_flat_set<CanonPath> && allowedPaths,
|
boost::unordered_flat_set<CanonPath, std::hash<CanonPath>> && allowedPaths,
|
||||||
MakeNotAllowedError && makeNotAllowedError);
|
MakeNotAllowedError && makeNotAllowedError);
|
||||||
|
|
||||||
using FilteringSourceAccessor::FilteringSourceAccessor;
|
using FilteringSourceAccessor::FilteringSourceAccessor;
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,6 @@
|
||||||
#include <set>
|
#include <set>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <boost/container_hash/hash.hpp>
|
|
||||||
|
|
||||||
namespace nix {
|
namespace nix {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -260,17 +258,11 @@ public:
|
||||||
*/
|
*/
|
||||||
std::string makeRelative(const CanonPath & path) const;
|
std::string makeRelative(const CanonPath & path) const;
|
||||||
|
|
||||||
friend std::size_t hash_value(const CanonPath &);
|
friend struct std::hash<CanonPath>;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::ostream & operator<<(std::ostream & stream, const CanonPath & path);
|
std::ostream & operator<<(std::ostream & stream, const CanonPath & path);
|
||||||
|
|
||||||
inline std::size_t hash_value(const CanonPath & path)
|
|
||||||
{
|
|
||||||
boost::hash<std::string_view> hasher;
|
|
||||||
return hasher(path.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace nix
|
} // namespace nix
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
|
|
@ -278,8 +270,8 @@ struct std::hash<nix::CanonPath>
|
||||||
{
|
{
|
||||||
using is_avalanching = std::true_type;
|
using is_avalanching = std::true_type;
|
||||||
|
|
||||||
std::size_t operator()(const nix::CanonPath & path) const noexcept
|
std::size_t operator()(const nix::CanonPath & s) const noexcept
|
||||||
{
|
{
|
||||||
return nix::hash_value(path);
|
return std::hash<std::string>{}(s.path);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -119,23 +119,15 @@ struct SourcePath
|
||||||
|
|
||||||
std::ostream & operator<<(std::ostream & str, const SourcePath & path);
|
std::ostream & operator<<(std::ostream & str, const SourcePath & path);
|
||||||
|
|
||||||
inline std::size_t hash_value(const SourcePath & path)
|
|
||||||
{
|
|
||||||
std::size_t hash = 0;
|
|
||||||
boost::hash_combine(hash, path.accessor->number);
|
|
||||||
boost::hash_combine(hash, path.path);
|
|
||||||
return hash;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace nix
|
} // namespace nix
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
struct std::hash<nix::SourcePath>
|
struct std::hash<nix::SourcePath>
|
||||||
{
|
{
|
||||||
using is_avalanching = std::true_type;
|
|
||||||
|
|
||||||
std::size_t operator()(const nix::SourcePath & s) const noexcept
|
std::size_t operator()(const nix::SourcePath & s) const noexcept
|
||||||
{
|
{
|
||||||
return nix::hash_value(s);
|
std::size_t hash = 0;
|
||||||
|
hash_combine(hash, s.accessor->number, s.path);
|
||||||
|
return hash;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -220,17 +220,6 @@ typename T::mapped_type * get(T & map, const K & key)
|
||||||
template<class T, typename K>
|
template<class T, typename K>
|
||||||
typename T::mapped_type * get(T && map, const K & key) = delete;
|
typename T::mapped_type * get(T && map, const K & key) = delete;
|
||||||
|
|
||||||
/**
|
|
||||||
* Look up a value in a `boost::concurrent_flat_map`.
|
|
||||||
*/
|
|
||||||
template<class T>
|
|
||||||
std::optional<typename T::mapped_type> getConcurrent(const T & map, const typename T::key_type & key)
|
|
||||||
{
|
|
||||||
std::optional<typename T::mapped_type> res;
|
|
||||||
map.cvisit(key, [&](auto & x) { res = x.second; });
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a value for the specified key from an associate container, or a default value if the key isn't present.
|
* Get a value for the specified key from an associate container, or a default value if the key isn't present.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,9 @@ std::optional<struct stat> PosixSourceAccessor::cachedLstat(const CanonPath & pa
|
||||||
// former is not hashable on libc++.
|
// former is not hashable on libc++.
|
||||||
Path absPath = makeAbsPath(path).string();
|
Path absPath = makeAbsPath(path).string();
|
||||||
|
|
||||||
if (auto res = getConcurrent(cache, absPath))
|
std::optional<Cache::mapped_type> res;
|
||||||
|
cache.cvisit(absPath, [&](auto & x) { res.emplace(x.second); });
|
||||||
|
if (res)
|
||||||
return *res;
|
return *res;
|
||||||
|
|
||||||
auto st = nix::maybeLstat(absPath.c_str());
|
auto st = nix::maybeLstat(absPath.c_str());
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue