| //===----------------------------------------------------------------------===// | |
| // | |
| // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | |
| // See https://llvm.org/LICENSE.txt for license information. | |
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | |
| // | |
| //===----------------------------------------------------------------------===// | |
| /// | |
| /// \file | |
| /// Implements \c ObjectStore and \c ActionCache on top of a dynamically loaded | |
| /// plugin that provides the C API in \c "llvm-c/CAS/PluginAPI_functions.h". | |
| /// | |
| /// The asynchronous entry points of the plugin API are not called yet; they | |
| /// will be wired up once \c ObjectStore and \c ActionCache grow asynchronous | |
| /// interfaces. | |
| /// | |
| //===----------------------------------------------------------------------===// | |
| #include "PluginAPI.h" | |
| #include "llvm/ADT/ScopeExit.h" | |
| #include "llvm/CAS/ActionCache.h" | |
| #include "llvm/CAS/ObjectStore.h" | |
| #include "llvm/Support/DynamicLibrary.h" | |
| #include "llvm/Support/Error.h" | |
| #include "llvm/Support/MemoryBuffer.h" | |
| using namespace llvm; | |
| using namespace llvm::cas; | |
| namespace { | |
| class PluginCASContext : public CASContext { | |
| public: | |
| void printIDImpl(raw_ostream &OS, const CASID &ID) const final; | |
| StringRef getHashSchemaIdentifier() const final { return SchemaName; } | |
| static Expected<std::shared_ptr<PluginCASContext>> | |
| create(StringRef PluginPath, StringRef OnDiskPath, | |
| ArrayRef<std::pair<std::string, std::string>> PluginArgs); | |
| ~PluginCASContext() { Functions.cas_dispose(c_cas); } | |
| llcas_functions_t Functions{}; | |
| llcas_cas_t c_cas = nullptr; | |
| std::string SchemaName; | |
| static Error errorAndDispose(char *c_err, const llcas_functions_t &Funcs) { | |
| Error E = createStringError(inconvertibleErrorCode(), c_err); | |
| Funcs.string_dispose(c_err); | |
| return E; | |
| } | |
| Error errorAndDispose(char *c_err) const { | |
| return errorAndDispose(c_err, Functions); | |
| } | |
| }; | |
| } // anonymous namespace | |
| void PluginCASContext::printIDImpl(raw_ostream &OS, const CASID &ID) const { | |
| ArrayRef<uint8_t> Hash = ID.getHash(); | |
| char *c_printed_id = nullptr; | |
| char *c_err = nullptr; | |
| if (Functions.digest_print(c_cas, llcas_digest_t{Hash.data(), Hash.size()}, | |
| &c_printed_id, &c_err)) | |
| report_fatal_error(errorAndDispose(c_err)); | |
| OS << c_printed_id; | |
| Functions.string_dispose(c_printed_id); | |
| } | |
| Expected<std::shared_ptr<PluginCASContext>> PluginCASContext::create( | |
| StringRef PluginPath, StringRef OnDiskPath, | |
| ArrayRef<std::pair<std::string, std::string>> PluginArgs) { | |
| auto reportError = [PluginPath](const Twine &Description) -> Error { | |
| std::error_code EC = inconvertibleErrorCode(); | |
| return createStringError(EC, "error loading '" + PluginPath + | |
| "': " + Description); | |
| }; | |
| SmallString<256> PathBuf = PluginPath; | |
| std::string ErrMsg; | |
| sys::DynamicLibrary Lib = | |
| sys::DynamicLibrary::getPermanentLibrary(PathBuf.c_str(), &ErrMsg); | |
| if (!Lib.isValid()) | |
| return reportError(ErrMsg); | |
| llcas_functions_t Functions{}; | |
| #define CASPLUGINAPI_FUNCTION(name, required) \ | |
| if (!(Functions.name = (decltype(llcas_functions_t::name)) \ | |
| Lib.getAddressOfSymbol("llcas_" #name))) { \ | |
| if (required) \ | |
| return reportError("failed symbol 'llcas_" #name "' lookup"); \ | |
| } | |
| #include "PluginAPI_functions.def" | |
| #undef CASPLUGINAPI_FUNCTION | |
| llcas_cas_options_t c_opts = Functions.cas_options_create(); | |
| scope_exit DisposeOptions([&]() { Functions.cas_options_dispose(c_opts); }); | |
| Functions.cas_options_set_client_version(c_opts, LLCAS_VERSION_MAJOR, | |
| LLCAS_VERSION_MINOR); | |
| SmallString<256> OnDiskPathBuf = OnDiskPath; | |
| Functions.cas_options_set_ondisk_path(c_opts, OnDiskPathBuf.c_str()); | |
| for (const auto &Pair : PluginArgs) { | |
| char *c_err = nullptr; | |
| if (Functions.cas_options_set_option(c_opts, Pair.first.c_str(), | |
| Pair.second.c_str(), &c_err)) | |
| return errorAndDispose(c_err, Functions); | |
| } | |
| char *c_err = nullptr; | |
| llcas_cas_t c_cas = Functions.cas_create(c_opts, &c_err); | |
| if (!c_cas) | |
| return errorAndDispose(c_err, Functions); | |
| char *c_schema = Functions.cas_get_hash_schema_name(c_cas); | |
| std::string SchemaName = c_schema; | |
| Functions.string_dispose(c_schema); | |
| auto Ctx = std::make_shared<PluginCASContext>(); | |
| Ctx->Functions = Functions; | |
| Ctx->c_cas = c_cas; | |
| Ctx->SchemaName = std::move(SchemaName); | |
| return Ctx; | |
| } | |
| //===----------------------------------------------------------------------===// | |
| // ObjectStore API | |
| //===----------------------------------------------------------------------===// | |
| namespace { | |
| class PluginObjectStore : public ObjectStore { | |
| public: | |
| Expected<CASID> parseID(StringRef ID) final; | |
| Expected<ObjectRef> store(ArrayRef<ObjectRef> Refs, | |
| ArrayRef<char> Data) final; | |
| Expected<ObjectRef> storeFromFile(StringRef Path) final; | |
| Error exportDataToFile(ObjectHandle Node, StringRef Path) const final; | |
| CASID getID(ObjectRef Ref) const final; | |
| std::optional<ObjectRef> getReference(const CASID &ID) const final; | |
| Expected<bool> isMaterialized(ObjectRef Ref) const final; | |
| Expected<std::optional<ObjectHandle>> loadIfExists(ObjectRef Ref) final; | |
| uint64_t getDataSize(ObjectHandle Node) const final; | |
| Error forEachRef(ObjectHandle Node, | |
| function_ref<Error(ObjectRef)> Callback) const final; | |
| ObjectRef readRef(ObjectHandle Node, size_t I) const final; | |
| size_t getNumRefs(ObjectHandle Node) const final; | |
| ArrayRef<char> getData(ObjectHandle Node, | |
| bool RequiresNullTerminator = false) const final; | |
| std::unique_ptr<MemoryBuffer> | |
| getStandaloneMemoryBufferImpl(ObjectHandle Node, StringRef Name, | |
| bool RequiresNullTerminator) final; | |
| Error validateObject(const CASID &ID) final { | |
| // Not supported yet. Always return success. | |
| return Error::success(); | |
| } | |
| Error validate(bool CheckHash) const final; | |
| Error setSizeLimit(std::optional<uint64_t> SizeLimit) final; | |
| Expected<std::optional<uint64_t>> getStorageSize() const final; | |
| Error pruneStorageData() final; | |
| PluginObjectStore(std::shared_ptr<PluginCASContext>); | |
| /// Exposes \c makeObjectRef to the file-local helpers below. | |
| ObjectRef makeRef(uint64_t InternalRef) const { | |
| return makeObjectRef(InternalRef); | |
| } | |
| std::shared_ptr<PluginCASContext> Ctx; | |
| }; | |
| } // anonymous namespace | |
| Expected<CASID> PluginObjectStore::parseID(StringRef ID) { | |
| // Use big enough stack so that we don't have to allocate in the heap. | |
| SmallString<148> IDBuf(ID); | |
| SmallVector<uint8_t, 68> BytesBuf(68); | |
| auto parseDigest = [&]() -> Expected<unsigned> { | |
| char *c_err = nullptr; | |
| unsigned NumBytes = Ctx->Functions.digest_parse( | |
| Ctx->c_cas, IDBuf.c_str(), BytesBuf.data(), BytesBuf.size(), &c_err); | |
| if (NumBytes == 0) | |
| return Ctx->errorAndDispose(c_err); | |
| return NumBytes; | |
| }; | |
| Expected<unsigned> NumBytes = parseDigest(); | |
| if (!NumBytes) | |
| return NumBytes.takeError(); | |
| if (*NumBytes > BytesBuf.size()) { | |
| BytesBuf.resize(*NumBytes); | |
| NumBytes = parseDigest(); | |
| if (!NumBytes) | |
| return NumBytes.takeError(); | |
| assert(*NumBytes == BytesBuf.size()); | |
| } else { | |
| BytesBuf.truncate(*NumBytes); | |
| } | |
| return CASID::create(Ctx.get(), toStringRef(BytesBuf)); | |
| } | |
| Expected<ObjectRef> PluginObjectStore::store(ArrayRef<ObjectRef> Refs, | |
| ArrayRef<char> Data) { | |
| SmallVector<llcas_objectid_t, 64> c_ids; | |
| c_ids.reserve(Refs.size()); | |
| for (ObjectRef Ref : Refs) { | |
| c_ids.push_back(llcas_objectid_t{Ref.getInternalRef(*this)}); | |
| } | |
| llcas_objectid_t c_stored_id; | |
| char *c_err = nullptr; | |
| if (Ctx->Functions.cas_store_object( | |
| Ctx->c_cas, llcas_data_t{Data.data(), Data.size()}, c_ids.data(), | |
| c_ids.size(), &c_stored_id, &c_err)) | |
| return Ctx->errorAndDispose(c_err); | |
| return makeObjectRef(c_stored_id.opaque); | |
| } | |
| Expected<ObjectRef> PluginObjectStore::storeFromFile(StringRef Path) { | |
| if (!Ctx->Functions.cas_store_from_filepath) | |
| return ObjectStore::storeFromFile(Path); | |
| llcas_objectid_t c_stored_id; | |
| char *c_err = nullptr; | |
| std::string PathStr = Path.str(); | |
| if (Ctx->Functions.cas_store_from_filepath(Ctx->c_cas, PathStr.c_str(), | |
| &c_stored_id, &c_err)) | |
| return Ctx->errorAndDispose(c_err); | |
| return makeObjectRef(c_stored_id.opaque); | |
| } | |
| Error PluginObjectStore::exportDataToFile(ObjectHandle Node, | |
| StringRef Path) const { | |
| if (!Ctx->Functions.loaded_object_export_data_to_filepath) | |
| return ObjectStore::exportDataToFile(Node, Path); | |
| char *c_err = nullptr; | |
| std::string PathStr = Path.str(); | |
| if (Ctx->Functions.loaded_object_export_data_to_filepath( | |
| Ctx->c_cas, llcas_loaded_object_t{Node.getInternalRef(*this)}, | |
| PathStr.c_str(), &c_err)) | |
| return Ctx->errorAndDispose(c_err); | |
| return Error::success(); | |
| } | |
| static StringRef toStringRef(llcas_digest_t c_digest) { | |
| return StringRef((const char *)c_digest.data, c_digest.size); | |
| } | |
| CASID PluginObjectStore::getID(ObjectRef Ref) const { | |
| llcas_objectid_t c_id{Ref.getInternalRef(*this)}; | |
| llcas_digest_t c_digest = | |
| Ctx->Functions.objectid_get_digest(Ctx->c_cas, c_id); | |
| return CASID::create(Ctx.get(), toStringRef(c_digest)); | |
| } | |
| std::optional<ObjectRef> | |
| PluginObjectStore::getReference(const CASID &ID) const { | |
| ArrayRef<uint8_t> Hash = ID.getHash(); | |
| llcas_objectid_t c_id; | |
| char *c_err = nullptr; | |
| if (Ctx->Functions.cas_get_objectid( | |
| Ctx->c_cas, llcas_digest_t{Hash.data(), Hash.size()}, &c_id, &c_err)) | |
| report_fatal_error(Ctx->errorAndDispose(c_err)); | |
| return makeObjectRef(c_id.opaque); | |
| } | |
| Expected<bool> PluginObjectStore::isMaterialized(ObjectRef Ref) const { | |
| llcas_objectid_t c_id{Ref.getInternalRef(*this)}; | |
| char *c_err = nullptr; | |
| llcas_lookup_result_t c_result = Ctx->Functions.cas_contains_object( | |
| Ctx->c_cas, c_id, /*globally=*/false, &c_err); | |
| switch (c_result) { | |
| case LLCAS_LOOKUP_RESULT_SUCCESS: | |
| return true; | |
| case LLCAS_LOOKUP_RESULT_NOTFOUND: | |
| return false; | |
| case LLCAS_LOOKUP_RESULT_ERROR: | |
| return Ctx->errorAndDispose(c_err); | |
| } | |
| llvm_unreachable("unknown llcas_lookup_result_t value"); | |
| } | |
| Expected<std::optional<ObjectHandle>> | |
| PluginObjectStore::loadIfExists(ObjectRef Ref) { | |
| llcas_objectid_t c_id{Ref.getInternalRef(*this)}; | |
| llcas_loaded_object_t c_obj; | |
| char *c_err = nullptr; | |
| llcas_lookup_result_t c_result = | |
| Ctx->Functions.cas_load_object(Ctx->c_cas, c_id, &c_obj, &c_err); | |
| switch (c_result) { | |
| case LLCAS_LOOKUP_RESULT_SUCCESS: | |
| return makeObjectHandle(c_obj.opaque); | |
| case LLCAS_LOOKUP_RESULT_NOTFOUND: | |
| return std::nullopt; | |
| case LLCAS_LOOKUP_RESULT_ERROR: | |
| return Ctx->errorAndDispose(c_err); | |
| } | |
| llvm_unreachable("unknown llcas_lookup_result_t value"); | |
| } | |
| namespace { | |
| class ObjectRefsWrapper { | |
| public: | |
| ObjectRefsWrapper(const ObjectHandle &Node, const PluginObjectStore &Store) | |
| : Store(Store), Ctx(*Store.Ctx) { | |
| llcas_loaded_object_t c_obj{Node.getInternalRef(Store)}; | |
| this->c_refs = Ctx.Functions.loaded_object_get_refs(Ctx.c_cas, c_obj); | |
| } | |
| size_t size() const { | |
| return Ctx.Functions.object_refs_get_count(Ctx.c_cas, c_refs); | |
| } | |
| ObjectRef operator[](size_t I) const { | |
| llcas_objectid_t c_id = | |
| Ctx.Functions.object_refs_get_id(Ctx.c_cas, c_refs, I); | |
| return Store.makeRef(c_id.opaque); | |
| } | |
| private: | |
| const PluginObjectStore &Store; | |
| PluginCASContext &Ctx; | |
| llcas_object_refs_t c_refs; | |
| }; | |
| } // namespace | |
| // FIXME: Replace forEachRef/readRef/getNumRefs APIs with an iterator interface. | |
| Error PluginObjectStore::forEachRef( | |
| ObjectHandle Node, function_ref<Error(ObjectRef)> Callback) const { | |
| ObjectRefsWrapper Refs(Node, *this); | |
| for (unsigned I = 0, E = Refs.size(); I != E; ++I) { | |
| if (Error E = Callback(Refs[I])) | |
| return E; | |
| } | |
| return Error::success(); | |
| } | |
| ObjectRef PluginObjectStore::readRef(ObjectHandle Node, size_t I) const { | |
| ObjectRefsWrapper Refs(Node, *this); | |
| return Refs[I]; | |
| } | |
| size_t PluginObjectStore::getNumRefs(ObjectHandle Node) const { | |
| ObjectRefsWrapper Refs(Node, *this); | |
| return Refs.size(); | |
| } | |
| // FIXME: Remove getDataSize(ObjectHandle) from API requirement, | |
| // \c getData(ObjectHandle) should be enough. | |
| uint64_t PluginObjectStore::getDataSize(ObjectHandle Node) const { | |
| ArrayRef<char> Data = getData(Node); | |
| return Data.size(); | |
| } | |
| ArrayRef<char> PluginObjectStore::getData(ObjectHandle Node, | |
| bool RequiresNullTerminator) const { | |
| // FIXME: Remove RequiresNullTerminator from ObjectStore API requirement? | |
| // It is a requirement for the plugin API. | |
| llcas_data_t c_data = Ctx->Functions.loaded_object_get_data( | |
| Ctx->c_cas, llcas_loaded_object_t{Node.getInternalRef(*this)}); | |
| return ArrayRef((const char *)c_data.data, c_data.size); | |
| } | |
| namespace { | |
| /// A MemoryBuffer over a plugin's standalone buffer, which it releases when | |
| /// destroyed. It holds the dispose function directly rather than the | |
| /// \c llcas_cas_t, since the point of the buffer is to outlive that. | |
| class PluginStandaloneMemoryBuffer final : public MemoryBuffer { | |
| public: | |
| using DisposeFn = void (*)(llcas_data_t); | |
| PluginStandaloneMemoryBuffer(llcas_data_t Data, StringRef Name, | |
| DisposeFn Dispose) | |
| : Data(Data), Name(Name.str()), Dispose(Dispose) { | |
| const char *Start = static_cast<const char *>(Data.data); | |
| init(Start, Start + Data.size, /*RequiresNullTerminator=*/true); | |
| } | |
| ~PluginStandaloneMemoryBuffer() override { Dispose(Data); } | |
| StringRef getBufferIdentifier() const final { return Name; } | |
| BufferKind getBufferKind() const final { return MemoryBuffer_Malloc; } | |
| private: | |
| llcas_data_t Data; | |
| std::string Name; | |
| DisposeFn Dispose; | |
| }; | |
| } // namespace | |
| std::unique_ptr<MemoryBuffer> PluginObjectStore::getStandaloneMemoryBufferImpl( | |
| ObjectHandle Node, StringRef Name, bool RequiresNullTerminator) { | |
| // Both halves are needed: without the disposer there is no way to release | |
| // what the getter hands out. | |
| if (!Ctx->Functions.loaded_object_get_standalone_data || | |
| !Ctx->Functions.standalone_data_dispose) | |
| return ObjectStore::getStandaloneMemoryBufferImpl(Node, Name, | |
| RequiresNullTerminator); | |
| llcas_data_t c_data = Ctx->Functions.loaded_object_get_standalone_data( | |
| Ctx->c_cas, llcas_loaded_object_t{Node.getInternalRef(*this)}); | |
| if (!c_data.data) | |
| return ObjectStore::getStandaloneMemoryBufferImpl(Node, Name, | |
| RequiresNullTerminator); | |
| return std::make_unique<PluginStandaloneMemoryBuffer>( | |
| c_data, Name, Ctx->Functions.standalone_data_dispose); | |
| } | |
| Error PluginObjectStore::setSizeLimit(std::optional<uint64_t> SizeLimit) { | |
| if (Ctx->Functions.cas_set_ondisk_size_limit) { | |
| char *c_err = nullptr; | |
| if (Ctx->Functions.cas_set_ondisk_size_limit(Ctx->c_cas, | |
| SizeLimit.value_or(0), &c_err)) | |
| return Ctx->errorAndDispose(c_err); | |
| } | |
| return Error::success(); | |
| } | |
| Expected<std::optional<uint64_t>> PluginObjectStore::getStorageSize() const { | |
| if (!Ctx->Functions.cas_get_ondisk_size) | |
| return std::nullopt; | |
| char *c_err = nullptr; | |
| int64_t ret = Ctx->Functions.cas_get_ondisk_size(Ctx->c_cas, &c_err); | |
| switch (ret) { | |
| case -1: | |
| return std::nullopt; | |
| case -2: | |
| return Ctx->errorAndDispose(c_err); | |
| default: | |
| return ret; | |
| } | |
| } | |
| Error PluginObjectStore::pruneStorageData() { | |
| if (Ctx->Functions.cas_prune_ondisk_data) { | |
| char *c_err = nullptr; | |
| if (Ctx->Functions.cas_prune_ondisk_data(Ctx->c_cas, &c_err)) | |
| return Ctx->errorAndDispose(c_err); | |
| } | |
| return Error::success(); | |
| } | |
| Error PluginObjectStore::validate(bool CheckHash) const { | |
| if (Ctx->Functions.cas_validate) { | |
| char *c_err = nullptr; | |
| if (Ctx->Functions.cas_validate(Ctx->c_cas, CheckHash, &c_err)) | |
| return Ctx->errorAndDispose(c_err); | |
| return Error::success(); | |
| } | |
| return createStringError("plugin cas doesn't support validation"); | |
| } | |
| PluginObjectStore::PluginObjectStore(std::shared_ptr<PluginCASContext> CASCtx) | |
| : ObjectStore(*CASCtx), Ctx(std::move(CASCtx)) {} | |
| //===----------------------------------------------------------------------===// | |
| // ActionCache API | |
| //===----------------------------------------------------------------------===// | |
| namespace { | |
| class PluginActionCache : public ActionCache { | |
| public: | |
| Expected<std::optional<CASID>> getImpl(ArrayRef<uint8_t> ResolvedKey, | |
| bool CanBeDistributed) const final; | |
| Error putImpl(ArrayRef<uint8_t> ResolvedKey, const CASID &Result, | |
| bool CanBeDistributed) final; | |
| PluginActionCache(std::shared_ptr<PluginCASContext>); | |
| Error validate() const final; | |
| private: | |
| std::shared_ptr<PluginCASContext> Ctx; | |
| }; | |
| } // anonymous namespace | |
| Expected<std::optional<CASID>> | |
| PluginActionCache::getImpl(ArrayRef<uint8_t> ResolvedKey, | |
| bool CanBeDistributed) const { | |
| llcas_objectid_t c_value; | |
| char *c_err = nullptr; | |
| llcas_lookup_result_t c_result = Ctx->Functions.actioncache_get_for_digest( | |
| Ctx->c_cas, llcas_digest_t{ResolvedKey.data(), ResolvedKey.size()}, | |
| &c_value, CanBeDistributed, &c_err); | |
| switch (c_result) { | |
| case LLCAS_LOOKUP_RESULT_SUCCESS: { | |
| llcas_digest_t c_digest = | |
| Ctx->Functions.objectid_get_digest(Ctx->c_cas, c_value); | |
| return CASID::create(Ctx.get(), toStringRef(c_digest)); | |
| } | |
| case LLCAS_LOOKUP_RESULT_NOTFOUND: | |
| return std::nullopt; | |
| case LLCAS_LOOKUP_RESULT_ERROR: | |
| return Ctx->errorAndDispose(c_err); | |
| } | |
| llvm_unreachable("unknown llcas_lookup_result_t value"); | |
| } | |
| Error PluginActionCache::putImpl(ArrayRef<uint8_t> ResolvedKey, | |
| const CASID &Result, bool CanBeDistributed) { | |
| ArrayRef<uint8_t> Hash = Result.getHash(); | |
| llcas_objectid_t c_value; | |
| char *c_err = nullptr; | |
| if (Ctx->Functions.cas_get_objectid(Ctx->c_cas, | |
| llcas_digest_t{Hash.data(), Hash.size()}, | |
| &c_value, &c_err)) | |
| return Ctx->errorAndDispose(c_err); | |
| if (Ctx->Functions.actioncache_put_for_digest( | |
| Ctx->c_cas, llcas_digest_t{ResolvedKey.data(), ResolvedKey.size()}, | |
| c_value, CanBeDistributed, &c_err)) | |
| return Ctx->errorAndDispose(c_err); | |
| return Error::success(); | |
| } | |
| PluginActionCache::PluginActionCache(std::shared_ptr<PluginCASContext> CASCtx) | |
| : ActionCache(*CASCtx), Ctx(std::move(CASCtx)) {} | |
| Error PluginActionCache::validate() const { | |
| if (Ctx->Functions.actioncache_validate) { | |
| char *c_err = nullptr; | |
| if (Ctx->Functions.actioncache_validate(Ctx->c_cas, &c_err)) | |
| return Ctx->errorAndDispose(c_err); | |
| return Error::success(); | |
| } | |
| return createStringError("plugin action cache doesn't support validation"); | |
| } | |
| //===----------------------------------------------------------------------===// | |
| // createPluginCASDatabases API | |
| //===----------------------------------------------------------------------===// | |
| Expected<std::pair<std::shared_ptr<ObjectStore>, std::shared_ptr<ActionCache>>> | |
| cas::createPluginCASDatabases( | |
| StringRef PluginPath, StringRef OnDiskPath, | |
| ArrayRef<std::pair<std::string, std::string>> PluginArgs) { | |
| std::shared_ptr<PluginCASContext> Ctx; | |
| if (Error E = PluginCASContext::create(PluginPath, OnDiskPath, PluginArgs) | |
| .moveInto(Ctx)) | |
| return std::move(E); | |
| auto CAS = std::make_shared<PluginObjectStore>(Ctx); | |
| auto AC = std::make_shared<PluginActionCache>(std::move(Ctx)); | |
| return std::make_pair(std::move(CAS), std::move(AC)); | |
| } |