/* * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or * its licensors. * * For complete copyright and license terms please see the LICENSE at the root of this * distribution (the "License"). All use of this software is governed by the License, * or, if provided, by the license below or the license accompanying this file. Do not * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * */ #include "ProjectSettings.h" #include "GemRegistry.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(AZ_PLATFORM_ANDROID) #include #endif #define MAX_ERROR_STRING_SIZE 512 namespace Gems { AZ_CLASS_ALLOCATOR_IMPL(ProjectSettings, AZ::SystemAllocator, 0) ProjectSettings::ProjectSettings(GemRegistry* registry) : m_registry(registry) , m_initialized(false) { } AZ::Outcome ProjectSettings::Initialize(const AZStd::string& appRootFolder, const AZStd::string& projectSubFolder) { AZ_Assert(!m_initialized, "ProjectSettings has been initialized already."); // Initialize the app root folder m_projectRootPath = appRootFolder; // Project gems file lives in (ProjectFolder)/gems.json - which might be @assets@/gems.json or an absolute path (in tools) m_gemsSettingsFilePath = appRootFolder; AzFramework::StringFunc::Path::Join(m_gemsSettingsFilePath.c_str(), projectSubFolder.c_str(), m_gemsSettingsFilePath); AzFramework::StringFunc::Path::Join(m_gemsSettingsFilePath.c_str(), GEMS_PROJECT_FILE, m_gemsSettingsFilePath); // Project config file lives in (ProjectFolder)/project.json - which might be @assets@/project.json or an absolute path (in tools) m_projectSettingsFilePath = appRootFolder; AzFramework::StringFunc::Path::Join(m_projectSettingsFilePath.c_str(), projectSubFolder.c_str(), m_projectSettingsFilePath); AzFramework::StringFunc::Path::Join(m_projectSettingsFilePath.c_str(), PROJECT_CONFIG_FILE, m_projectSettingsFilePath); auto loadOutcome = LoadSettings(); m_initialized = loadOutcome.IsSuccess(); return loadOutcome; } bool ProjectSettings::EnableGem(const ProjectGemSpecifier& spec) { auto it = m_gems.find(spec.m_id); if (it != m_gems.end()) { // If the Gem is already enabled, update the version and path of the entry. it->second.m_version = spec.m_version; it->second.m_path = spec.m_path; } else { // create entry based on data from registry m_gems.insert(AZStd::make_pair(spec.m_id, spec)); } return true; } bool ProjectSettings::DisableGem(const GemSpecifier& spec) { auto it = m_gems.find(spec.m_id); // If the Gem is enabled at the version specified, disable it. if (it != m_gems.end()) { if (spec.m_version != it->second.m_version) { return false; } m_gems.erase(it); } return true; } bool ProjectSettings::IsGemEnabled(const GemSpecifier& spec) const { auto it = m_gems.find(spec.m_id); return it != m_gems.end() && it->second.m_version == spec.m_version; } bool ProjectSettings::IsGemEnabled(const AZ::Uuid& id, const AZStd::vector& versionConstraints) const { AZStd::shared_ptr dependency = AZStd::make_shared(); dependency->SetID(id); auto parseOutcome = dependency->ParseVersions(versionConstraints); if (!parseOutcome.IsSuccess()) { AZ_Assert(false, parseOutcome.GetError().c_str()); return false; } return IsGemDependencyMet(dependency); } bool ProjectSettings::IsGemDependencyMet(const AZStd::shared_ptr dep) const { // Gems can depend on other Gems auto it = m_gems.find(dep->GetID()); return it != m_gems.end() && dep->IsFullfilledBy(it->second); } bool ProjectSettings::IsEngineDependencyMet(const AZStd::shared_ptr dep, const EngineVersion& againstVersion) const { EngineSpecifier engineSpecifier(AZ::Uuid::CreateNull(), againstVersion); return dep->IsFullfilledBy(engineSpecifier); } class GemDependencyInfo : public GemDependency { public: GemDependencyInfo(IGemDescriptionConstPtr gem) : GemDependency() , m_gem{gem} { } IGemDescriptionConstPtr GetGem() const { return m_gem; } private: IGemDescriptionConstPtr m_gem; }; AZ::Outcome ProjectSettings::ValidateDependencies(const EngineVersion& engineVersion) const { AZStd::unordered_map globalDeps; // Build list of required Gems for (const auto& pair : m_gems) { const ProjectGemSpecifier& spec = pair.second; auto gem = m_registry->GetGemDescription(spec); if (!gem) { return AZ::Failure(AZStd::string::format("Gem with Id \"%s\" not found.", pair.first.ToString().c_str())); } for (auto && gemDep : gem->GetGemDependencies()) { const AZ::Uuid id = gemDep->GetID(); GemDependency* dep; // If the dependency isn't tracked globally, create a new one auto depIt = globalDeps.find(id); if (depIt == globalDeps.end()) { globalDeps.insert(AZStd::make_pair(id, GemDependencyInfo(gem))); dep = &globalDeps.at(id); dep->m_id = id; } else { dep = &depIt->second; } // These bounds should be normalized before verification to make sure there aren't conflicting bounds dep->m_bounds.insert(dep->m_bounds.end(), gemDep->GetBounds().begin(), gemDep->GetBounds().end()); } } AZStd::string errorString; bool isTreeValid = true; // Verify all engine dependencies are met for(const auto& pair : m_gems) { const ProjectGemSpecifier& spec = pair.second; auto gem = m_registry->GetGemDescription(spec); if (!gem) { errorString += AZStd::string::format("Gem with Id \"%s\" not found.", pair.first.ToString().c_str()); isTreeValid = false; continue; } // do not verify the engine version if input is default constructed if (engineVersion == EngineVersion()) { continue; } // Check the Gem's engine dependency auto engineDepPtr = gem->GetEngineDependency(); if (engineDepPtr && !IsEngineDependencyMet(engineDepPtr, engineVersion)) { AZStd::string errmsg = AZStd::string::format("Gem with Id \"%s\" does not meet the Lumberyard engine version requirement.\n", pair.first.ToString().c_str()); // do not force an assertion to happen here, we are just printing the warning and letting the user // decide on how to handle it if the engine start up fails. errorString += errmsg; AZ_Warning("GemRegistry", false, errmsg.c_str()); } } // attempt to construct a complete gem registry for unmet dependency ID to name resolution GemRegistry completeRegistry; const char* gemsSearchFilter = "Gems"; const char* appRoot = nullptr; AzFramework::ApplicationRequests::Bus::BroadcastResult(appRoot, &AzFramework::ApplicationRequests::GetAppRoot); if (appRoot) { completeRegistry.AddSearchPath({ appRoot, gemsSearchFilter }, false); } const char* engineRoot = nullptr; AzFramework::ApplicationRequests::Bus::BroadcastResult(engineRoot, &AzFramework::ApplicationRequests::GetEngineRoot); if (engineRoot) { completeRegistry.AddSearchPath({ engineRoot, gemsSearchFilter }, false); } completeRegistry.LoadAllGemsFromDisk(); // Verify all gems dependencies are all met for (auto && pair : globalDeps) { const GemDependencyInfo& dep = pair.second; // Find candidate in project's listed gems auto candidateIt = m_gems.find(dep.GetID()); if (candidateIt == m_gems.end()) { // no candidate found char depIdStr[UUID_STR_BUF_LEN]; dep.GetID().ToString(depIdStr, UUID_STR_BUF_LEN, true, true); char gemIdStr[UUID_STR_BUF_LEN]; dep.GetGem()->GetID().ToString(gemIdStr, UUID_STR_BUF_LEN, true, true); // don't care about the version, just need the gem name IGemDescriptionConstPtr depDesc = completeRegistry.GetLatestGem(dep.GetID()); if (depDesc) { errorString += AZStd::string::format( "Gem \"%s\" (%s) dependency on Gem \"%s\" (%s) is unmet.\n", dep.GetGem()->GetDisplayName().c_str(), gemIdStr, depDesc->GetDisplayName().c_str(), depIdStr ); } else { errorString += AZStd::string::format( "Gem \"%s\" (%s) dependency on unresolved Gem with ID %s is unmet.\n", dep.GetGem()->GetDisplayName().c_str(), gemIdStr, depIdStr ); } isTreeValid = false; } else if (!dep.IsFullfilledBy(candidateIt->second)) { // candidate found, but it doesn't fulfill all dependency requirements AZStd::string boundsStr; for (auto && bound : dep.m_bounds) { if (boundsStr.length() == 0) { boundsStr = bound.ToString(); } else { boundsStr += ", " + bound.ToString(); } } char depIdStr[UUID_STR_BUF_LEN]; dep.GetID().ToString(depIdStr, UUID_STR_BUF_LEN, true, true); char gemIdStr[UUID_STR_BUF_LEN]; dep.GetGem()->GetID().ToString(gemIdStr, UUID_STR_BUF_LEN, true, true); // don't care about the version, just need the gem name IGemDescriptionConstPtr depDesc = completeRegistry.GetLatestGem(dep.GetID()); if (depDesc) { errorString += AZStd::string::format( "Gem \"%s\" (%s) dependency on Gem \"%s\" (%s) is unmet. It must fall within the following version bounds: [%s]\n", dep.GetGem()->GetDisplayName().c_str(), gemIdStr, depDesc->GetDisplayName().c_str(), depIdStr, boundsStr.c_str() ); } else { errorString += AZStd::string::format( "Gem \"%s\" (%s) dependency on unresolved Gem with ID %s is unmet. It must fall within the following version bounds: [%s]\n", dep.GetGem()->GetDisplayName().c_str(), gemIdStr, depIdStr, boundsStr.c_str() ); } isTreeValid = false; } } if (!isTreeValid) { return AZ::Failure(errorString); } return AZ::Success(); } AZ::Outcome ProjectSettings::Save() const { using namespace AZ::IO; FileIOBase* fileIo = FileIOBase::GetInstance(); HandleType projectSettingsHandle = InvalidHandle; if (fileIo->Open(m_gemsSettingsFilePath.c_str(), OpenMode::ModeWrite | OpenMode::ModeText, projectSettingsHandle)) { rapidjson::Document jsonRep = GetJsonRepresentation(); rapidjson::StringBuffer buffer; rapidjson::PrettyWriter writer(buffer); jsonRep.Accept(writer); AZ::u64 bytesWritten = 0; if (!fileIo->Write(projectSettingsHandle, buffer.GetString(), buffer.GetSize(), &bytesWritten)) { return AZ::Failure(AZStd::string::format("Failed to write Gems settings to file: %s", m_gemsSettingsFilePath.c_str())); } if (bytesWritten != buffer.GetSize()) { return AZ::Failure(AZStd::string::format("Failed to write complete Gems settings to file: %s", m_gemsSettingsFilePath.c_str())); } fileIo->Close(projectSettingsHandle); return AZ::Success(); } else { char errorBuffer[MAX_ERROR_STRING_SIZE]; #if defined(AZ_PLATFORM_WINDOWS) FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, nullptr, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), errorBuffer, MAX_ERROR_STRING_SIZE, nullptr); #else azstrerror_s(errorBuffer, MAX_ERROR_STRING_SIZE, errno); #endif // defined(AZ_PLATFORM_WINDOWS) return AZ::Failure(AZStd::string::format("Failed to open %s for write: %s", m_gemsSettingsFilePath.c_str(), errorBuffer)); } } const AZStd::string& ProjectSettings::GetProjectName() const { return m_projectName; } const AZStd::string& ProjectSettings::GetProjectRootPath() const { return m_projectRootPath; } AZ::Outcome ProjectSettings::LoadSettings() { // an engine compatible file reader has been attached, so use that. AZ::IO::FileIOBase* fileReader = AZ::IO::FileIOBase::GetInstance(); // Read and parse the gems.json file { auto readGemsJsonResult = AzFramework::FileFunc::ReadJsonFile(m_gemsSettingsFilePath, fileReader); if (!readGemsJsonResult.IsSuccess()) { return AZ::Failure(AZStd::string::format("Failed to read Json file %s: %s", m_gemsSettingsFilePath.c_str(), readGemsJsonResult.GetError().c_str())); } auto parseGemsJsonResult = ParseGemsJson(readGemsJsonResult.GetValue()); if (!parseGemsJsonResult.IsSuccess()) { return AZ::Failure(AZStd::string::format("Failed to parse Json file %s: %s", m_gemsSettingsFilePath.c_str(), parseGemsJsonResult.GetError().c_str())); } } // Read and parse the project.json file { auto readProjectJsonResult = AzFramework::FileFunc::ReadJsonFile(m_projectSettingsFilePath, fileReader); if (!readProjectJsonResult.IsSuccess()) { return AZ::Failure(AZStd::string::format("Failed to read Json file %s: %s", m_gemsSettingsFilePath.c_str(), readProjectJsonResult.GetError().c_str())); } auto parseProjectJsonResult = ParseProjectJson(readProjectJsonResult.GetValue()); if (!parseProjectJsonResult.IsSuccess()) { return AZ::Failure(AZStd::string::format("Failed to parse Json file %s: %s", m_gemsSettingsFilePath.c_str(), parseProjectJsonResult.GetError().c_str())); } } return AZ::Success(); } rapidjson::Document ProjectSettings::GetJsonRepresentation() const { rapidjson::Document rootObj(rapidjson::kObjectType); rootObj.AddMember(GPF_TAG_LIST_FORMAT_VERSION, GEMS_PROJECT_FILE_VERSION, rootObj.GetAllocator()); // We want to write out Gems in the same order each time. // Create vector for sorting. AZStd::vector sortedGems(m_gems.size()); auto transformFn = [](const ProjectGemSpecifierMap::value_type& pair) { return &pair.second; }; AZStd::transform(m_gems.begin(), m_gems.end(), sortedGems.begin(), transformFn); // we'll sort based on ID. AZStd::sort(sortedGems.begin(), sortedGems.end(), [](const ProjectGemSpecifier* a, const ProjectGemSpecifier* b) -> bool { return a->m_id < b->m_id; }); auto addMember = [&rootObj](rapidjson::Value& obj, const char* key, const char* str) { rapidjson::Value k(rapidjson::StringRef(key), rootObj.GetAllocator()); rapidjson::Value v(rapidjson::StringRef(str), rootObj.GetAllocator()); obj.AddMember(k.Move(), v.Move(), rootObj.GetAllocator()); }; // build Gems array rapidjson::Value gemsArray(rapidjson::kArrayType); for (const ProjectGemSpecifier* gemSpec : sortedGems) { char idStr[UUID_STR_BUF_LEN]; gemSpec->m_id.ToString(idStr, UUID_STR_BUF_LEN, false, false); AZStd::to_lower(idStr, idStr + strlen(idStr)); AZStd::string path = gemSpec->m_path; // Replace '\' with '/' AZStd::replace(path.begin(), path.end(), '\\', '/'); // Remove trailing slash if (*path.rbegin() == '/') { path.pop_back(); } rapidjson::Value gemObj(rapidjson::kObjectType); addMember(gemObj, GPF_TAG_PATH, path.c_str()); addMember(gemObj, GPF_TAG_UUID, idStr); addMember(gemObj, GPF_TAG_VERSION, gemSpec->m_version.ToString().c_str()); // write name in comment (if possible) if (IGemDescriptionConstPtr gemDesc = m_registry->GetGemDescription(*gemSpec)) { addMember(gemObj, GPF_TAG_COMMENT, gemDesc->GetName().c_str()); } gemsArray.PushBack(gemObj, rootObj.GetAllocator()); } rootObj.AddMember(GPF_TAG_GEM_ARRAY, gemsArray, rootObj.GetAllocator()); return rootObj; } AZ::Outcome ProjectSettings::ParseGemsJson(const rapidjson::Document& jsonRep) { // check version if (!RAPIDJSON_IS_VALID_MEMBER(jsonRep, GPF_TAG_LIST_FORMAT_VERSION, IsInt)) { return AZ::Failure(AZStd::string(GPF_TAG_LIST_FORMAT_VERSION " number is required.")); } int gemListFormatVersion = jsonRep[GPF_TAG_LIST_FORMAT_VERSION].GetInt(); if (gemListFormatVersion != GEMS_PROJECT_FILE_VERSION) { return AZ::Failure(AZStd::string::format( GPF_TAG_LIST_FORMAT_VERSION " is version %d, but %d is expected.", gemListFormatVersion, GEMS_PROJECT_FILE_VERSION)); } // read gems if (!RAPIDJSON_IS_VALID_MEMBER(jsonRep, GPF_TAG_GEM_ARRAY, IsArray)) { return AZ::Failure(AZStd::string(GPF_TAG_GEM_ARRAY " list is required")); } const rapidjson::Value& gemList = jsonRep[GPF_TAG_GEM_ARRAY]; const auto& end = gemList.End(); for (auto it = gemList.Begin(); it != end; ++it) { const auto& elem = *it; // gem id if (!RAPIDJSON_IS_VALID_MEMBER(elem, GPF_TAG_UUID, IsString)) { return AZ::Failure(AZStd::string(GPF_TAG_UUID " string is required for Gem.")); } const char* idStr = elem[GPF_TAG_UUID].GetString(); AZ::Uuid id = AZ::Uuid::CreateString(idStr); if (id.IsNull()) { return AZ::Failure(AZStd::string(GPF_TAG_UUID " string is invalid for Gem.")); } // gem version if (!RAPIDJSON_IS_VALID_MEMBER(elem, GPF_TAG_VERSION, IsString)) { return AZ::Failure(AZStd::string::format( GPF_TAG_VERSION " string is missing for Gem with ID %s.", idStr)); } auto versionOutcome = GemVersion::ParseFromString(elem[GPF_TAG_VERSION].GetString()); if (!versionOutcome) { return AZ::Failure(AZStd::string::format( GPF_TAG_VERSION " string is invalid for Gem with ID %s: %s", idStr, versionOutcome.GetError().c_str())); } GemVersion version = versionOutcome.GetValue(); // gem path if (!RAPIDJSON_IS_VALID_MEMBER(elem, GPF_TAG_PATH, IsString)) { return AZ::Failure(AZStd::string(GPF_TAG_PATH " string is required for Gem")); } const char* path = elem[GPF_TAG_PATH].GetString(); EnableGem(ProjectGemSpecifier(id, version, path)); } return AZ::Success(); } AZ::Outcome ProjectSettings::ParseProjectJson(const rapidjson::Document& json) { // For now, we only static const char* project_name_key = "project_name"; if (!RAPIDJSON_IS_VALID_MEMBER(json, project_name_key, IsString)) { return AZ::Failure(AZStd::string::format("Missing/Invalid key '%s' in project.json.", project_name_key)); } m_projectName = json[project_name_key].GetString(); return AZ::Success(); } } // namespace Gems