// SPDX-License-Identifier: MIT #include "Common/StringConv.h" #include "Utils/Config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace FEXCore::Context { class Context; } namespace FEXCore::Config { namespace detail { #define P(x) x #define OPT_BASE(type, group, enum, json, default) const P(type) P(enum) = P(default); #define OPT_STR(group, enum, json, default) const std::string_view P(enum) = P(default); #define OPT_STRARRAY(group, enum, json, default) OPT_STR(group, enum, json, default) #define OPT_STRENUM(group, enum, json, default) const uint64_t P(enum) = FEXCore::ToUnderlying(P(default)); #include constexpr static std::array option_names = { #define OPT_BASE(type, group, enum, json, default) #json, #include }; } // namespace detail std::string_view GetConfigJSONName(FEXCore::Config::ConfigOption option) { return FEXCore::Config::detail::option_names[option]; } enum Paths { PATH_DATA_DIR_LOCAL = 0, PATH_DATA_DIR_GLOBAL, PATH_CONFIG_DIR_LOCAL, PATH_CONFIG_DIR_GLOBAL, PATH_CONFIG_FILE_LOCAL, PATH_CONFIG_FILE_GLOBAL, PATH_CONFIG_TELEMETRY_FOLDER, PATH_CACHE_DIR, PATH_LAST, }; static std::array Paths; void SetDataDirectory(const std::string_view Path, bool Global) { Paths[PATH_DATA_DIR_LOCAL + Global] = Path; } void SetConfigDirectory(const std::string_view Path, bool Global) { Paths[PATH_CONFIG_DIR_LOCAL + Global] = Path; } void SetConfigFileLocation(const std::string_view Path, bool Global) { Paths[PATH_CONFIG_FILE_LOCAL + Global] = Path; } void SetCacheDirectory(const std::string_view Path) { Paths[PATH_CACHE_DIR] = Path; } const fextl::string& GetTelemetryDirectory() { auto& Path = Paths[PATH_CONFIG_TELEMETRY_FOLDER]; if (Path.empty()) { FEX_CONFIG_OPT(TelemetryDirectory, TELEMETRYDIRECTORY); if (!TelemetryDirectory().empty()) { Path = TelemetryDirectory; Path += "/"; } else { Path = Config::GetDataDirectory(false) + "Telemetry/"; } } return Path; } const fextl::string& GetDataDirectory(bool Global) { return Paths[PATH_DATA_DIR_LOCAL + Global]; } const fextl::string& GetConfigDirectory(bool Global) { return Paths[PATH_CONFIG_DIR_LOCAL + Global]; } const fextl::string& GetConfigFileLocation(bool Global) { return Paths[PATH_CONFIG_FILE_LOCAL + Global]; } const fextl::string& GetCacheDirectory() { return Paths[PATH_CACHE_DIR]; } fextl::string GetApplicationConfig(const std::string_view Program, bool Global) { fextl::string ConfigFile = GetConfigDirectory(Global); if (!Global && !FHU::Filesystem::Exists(ConfigFile) && !FHU::Filesystem::CreateDirectories(ConfigFile)) { LogMan::Msg::DFmt("Couldn't create config directory: '{}'", ConfigFile); // Let's go local in this case return fextl::fmt::format("./{}.json", Program); } ConfigFile += "AppConfig/"; // Attempt to create the local folder if it doesn't exist if (!Global && !FHU::Filesystem::Exists(ConfigFile) && !FHU::Filesystem::CreateDirectories(ConfigFile)) { // Let's go local in this case return fextl::fmt::format("./{}.json", Program); } return fextl::fmt::format("{}{}.json", ConfigFile, Program); } static fextl::map> ConfigLayers; class MetaLayer; static FEXCore::Config::MetaLayer* Meta {}; constexpr std::array LoadOrder = { FEXCore::Config::LayerType::LAYER_GLOBAL_MAIN, FEXCore::Config::LayerType::LAYER_MAIN, FEXCore::Config::LayerType::LAYER_GLOBAL_STEAM_APP, FEXCore::Config::LayerType::LAYER_GLOBAL_APP, FEXCore::Config::LayerType::LAYER_LOCAL_STEAM_APP, FEXCore::Config::LayerType::LAYER_LOCAL_APP, FEXCore::Config::LayerType::LAYER_ARGUMENTS, FEXCore::Config::LayerType::LAYER_USER_OVERRIDE, FEXCore::Config::LayerType::LAYER_ENVIRONMENT, FEXCore::Config::LayerType::LAYER_TOP}; Layer::Layer(const LayerType _Type) : Type {_Type} {} Layer::~Layer() {} class MetaLayer final : public FEXCore::Config::Layer { public: MetaLayer(const LayerType _Type) : FEXCore::Config::Layer(_Type) {} ~MetaLayer() {} void Load(); template requires (!std::is_same_v && !std::is_same_v) std::optional GetConv(ConfigOption Option) { const auto it = OptionMap.find(Option); if (it == OptionMap.end()) { return std::nullopt; } const auto& Value = it->second; LOGMAN_THROW_A_FMT(!std::holds_alternative(Value), "Tried to get config of invalid type!"); if (std::holds_alternative(Value)) [[likely]] { return std::get(Value); } T ConvertedValue; if (std::holds_alternative(Value)) { const auto& StrVal = std::get(Value); if (FEXCore::StrConv::Conv(StrVal, &ConvertedValue)) { // Convert the value. OptionMap[Option].emplace(ConvertedValue); return ConvertedValue; } else { LOGMAN_MSG_A_FMT("Couldn't Convert {} to specified type!", StrVal); } } FEX_UNREACHABLE; } private: void MergeConfigMap(const LayerOptions& Options); void MergeEnvironmentVariables(const ConfigOption& Option, const StringArrayType& Value); }; void MetaLayer::Load() { OptionMap.clear(); for (auto CurrentLayer = LoadOrder.begin(); CurrentLayer != LoadOrder.end(); ++CurrentLayer) { auto it = ConfigLayers.find(*CurrentLayer); if (it != ConfigLayers.end() && *CurrentLayer != Type) { // Merge this layer's options to this layer MergeConfigMap(it->second->GetOptionMap()); } } } void MetaLayer::MergeEnvironmentVariables(const ConfigOption& Option, const StringArrayType& Value) { // Environment variables need a bit of additional work // We want to merge the arrays rather than overwrite entirely auto MetaEnvironment = OptionMap.find(Option); if (MetaEnvironment == OptionMap.end()) { // Doesn't exist, just insert OptionMap.insert_or_assign(Option, Value); return; } // If an environment variable exists in both current meta and in the incoming layer then the meta layer value is overwritten fextl::unordered_map LookupMap; const auto AddToMap = [&LookupMap](const StringArrayType& Value) { for (const auto& EnvVar : Value) { const auto ItEq = EnvVar.find_first_of('='); if (ItEq == fextl::string::npos) { // Broken environment variable // Skip continue; } auto Key = fextl::string(EnvVar.begin(), EnvVar.begin() + ItEq); auto Value = fextl::string(EnvVar.begin() + ItEq + 1, EnvVar.end()); // Add the key to the map, overwriting whatever previous value was there LookupMap.insert_or_assign(std::move(Key), std::move(Value)); } }; AddToMap(std::get(MetaEnvironment->second)); AddToMap(Value); // Now with the two layers merged in the map // Add all the values to the option Erase(Option); for (auto& Val : LookupMap) { // Set will emplace multiple options in to its list AppendStrArrayValue(Option, Val.first + "=" + Val.second); } } void MetaLayer::MergeConfigMap(const LayerOptions& Options) { // Insert this layer's options, overlaying previous options that exist here for (auto& it : Options) { if (it.first == FEXCore::Config::ConfigOption::CONFIG_ENV || it.first == FEXCore::Config::ConfigOption::CONFIG_HOSTENV) { LOGMAN_THROW_A_FMT(std::holds_alternative(it.second), "Tried to get config of invalid type!"); MergeEnvironmentVariables(it.first, std::get(it.second)); } else { OptionMap.insert_or_assign(it.first, it.second); } } } void Initialize() { AddLayer(fextl::make_unique(FEXCore::Config::LayerType::LAYER_TOP)); Meta = dynamic_cast(ConfigLayers.begin()->second.get()); } void Shutdown() { ConfigLayers.clear(); Meta = nullptr; } void Load() { for (auto CurrentLayer = LoadOrder.begin(); CurrentLayer != LoadOrder.end(); ++CurrentLayer) { auto it = ConfigLayers.find(*CurrentLayer); if (it != ConfigLayers.end()) { it->second->Load(); } } } static fextl::string ExpandPath(const fextl::string& ContainerPrefix, const fextl::string& PathName) { if (PathName.empty()) { return {}; } // Expand home if it exists if (FHU::Filesystem::IsRelative(PathName)) { fextl::string Home = getenv("HOME") ?: ""; // Home expansion only works if it is the first character // This matches bash behaviour if (PathName.starts_with("~/")) { Home.append(PathName.begin() + 1, PathName.end()); return Home; } // Expand relative path to absolute char ExistsTempPath[PATH_MAX]; char* RealPath = FHU::Filesystem::Absolute(PathName.c_str(), ExistsTempPath); if (RealPath && FHU::Filesystem::Exists(RealPath)) { return RealPath; } // Only return if it exists if (FHU::Filesystem::Exists(PathName)) { return PathName; } } else { // If the containerprefix and pathname isn't empty // Then we check if the pathname exists in our current namespace // If the path DOESN'T exist but DOES exist with the prefix applied // then redirect to the prefix // // This might not be expected behaviour for some edge cases but since // all paths aren't mounted inside the container, then it'll be fine // // Main catch case for this is the default thunk install folders // HostThunks: $CMAKE_INSTALL_PREFIX/lib/fex-emu/HostThunks/ // GuestThunks: $CMAKE_INSTALL_PREFIX/share/fex-emu/GuestThunks/ if (!ContainerPrefix.empty() && !PathName.empty()) { if (!FHU::Filesystem::Exists(PathName)) { auto ContainerPath = ContainerPrefix + PathName; if (FHU::Filesystem::Exists(ContainerPath)) { return ContainerPath; } } } } return {}; } constexpr char ContainerManager[] = "/run/host/container-manager"; fextl::string FindContainer() { // We only support pressure-vessel at the moment if (FHU::Filesystem::Exists(ContainerManager)) { fextl::string Manager {}; if (FEXCore::FileLoading::LoadFile(Manager, ContainerManager)) { // Trim the whitespace, may contain a newline return FEXCore::StringUtils::Trim(Manager); } } return {}; } fextl::string FindContainerPrefix() { // We only support pressure-vessel at the moment if (FHU::Filesystem::Exists(ContainerManager)) { fextl::string Manager {}; if (FEXCore::FileLoading::LoadFile(Manager, ContainerManager)) { // Trim the whitespace, may contain a newline if (FEXCore::StringUtils::Trim(Manager) == "pressure-vessel") { // We are running inside of pressure vessel // Our $CMAKE_INSTALL_PREFIX paths are now inside of /run/host/$CMAKE_INSTALL_PREFIX return "/run/host/"; } } } return {}; } void ReloadMetaLayer() { Meta->Load(); const fextl::string ContainerPrefix {FindContainerPrefix()}; auto ExpandPathIfExists = [&ContainerPrefix](FEXCore::Config::ConfigOption Config, const fextl::string& PathName) { const auto NewPath = ExpandPath(ContainerPrefix, PathName); if (!NewPath.empty()) { FEXCore::Config::Set(Config, NewPath); } }; if (FEXCore::Config::Exists(FEXCore::Config::CONFIG_ROOTFS)) { const auto PathName = *Meta->Get(FEXCore::Config::CONFIG_ROOTFS); const auto ExpandedString = ExpandPath(ContainerPrefix, *PathName); if (!ExpandedString.empty()) { // Adjust the path if it ended up being relative FEXCore::Config::Set(FEXCore::Config::CONFIG_ROOTFS, ExpandedString); } else if (!PathName->empty()) { // If the filesystem doesn't exist then let's see if it exists in the fex-emu folder const auto PathNameCopy = *PathName; for (auto Global : {true, false}) { for (auto DirectoryFetchers : {GetDataDirectory, GetConfigDirectory}) { fextl::string NamedRootFS = DirectoryFetchers(Global) + "RootFS/" + PathNameCopy; if (FHU::Filesystem::Exists(NamedRootFS)) { FEXCore::Config::Set(FEXCore::Config::CONFIG_ROOTFS, NamedRootFS); } } } } } if (FEXCore::Config::Exists(FEXCore::Config::CONFIG_THUNKHOSTLIBS)) { const auto PathName = *Meta->Get(FEXCore::Config::CONFIG_THUNKHOSTLIBS); ExpandPathIfExists(FEXCore::Config::CONFIG_THUNKHOSTLIBS, *PathName); } if (FEXCore::Config::Exists(FEXCore::Config::CONFIG_THUNKGUESTLIBS)) { const auto PathName = *Meta->Get(FEXCore::Config::CONFIG_THUNKGUESTLIBS); ExpandPathIfExists(FEXCore::Config::CONFIG_THUNKGUESTLIBS, *PathName); } if (FEXCore::Config::Exists(FEXCore::Config::CONFIG_THUNKCONFIG)) { const auto PathName = *Meta->Get(FEXCore::Config::CONFIG_THUNKCONFIG); const auto ExpandedString = ExpandPath(ContainerPrefix, *PathName); if (!ExpandedString.empty()) { // Adjust the path if it ended up being relative FEXCore::Config::Set(FEXCore::Config::CONFIG_THUNKCONFIG, ExpandedString); } else if (!PathName->empty()) { // If the filesystem doesn't exist then let's see if it exists in the fex-emu folder const auto PathNameCopy = *PathName; for (auto Global : {true, false}) { for (auto DirectoryFetchers : {GetDataDirectory, GetConfigDirectory}) { fextl::string NamedConfig = DirectoryFetchers(Global) + "ThunkConfigs/" + PathNameCopy; if (FHU::Filesystem::Exists(NamedConfig)) { FEXCore::Config::Set(FEXCore::Config::CONFIG_THUNKCONFIG, NamedConfig); } } } } } if (FEXCore::Config::Exists(FEXCore::Config::CONFIG_OUTPUTLOG)) { const auto PathName = *Meta->Get(FEXCore::Config::CONFIG_OUTPUTLOG); if (*PathName != "stdout" && *PathName != "stderr" && *PathName != "server") { ExpandPathIfExists(FEXCore::Config::CONFIG_OUTPUTLOG, *PathName); } } if (FEXCore::Config::Exists(FEXCore::Config::CONFIG_DUMPIR) && !FEXCore::Config::Exists(FEXCore::Config::CONFIG_PASSMANAGERDUMPIR)) { // If DumpIR is set but no PassManagerDumpIR configuration is set, then default to `afteropt` const auto PathName = *Meta->Get(FEXCore::Config::CONFIG_DUMPIR); if (*PathName != "no") { Set(FEXCore::Config::ConfigOption::CONFIG_PASSMANAGERDUMPIR, fextl::fmt::format("{}", static_cast(FEXCore::Config::PassManagerDumpIR::AFTEROPT))); } } if (FEXCore::Config::Exists(FEXCore::Config::CONFIG_SINGLESTEP) && Meta->GetConv(FEXCore::Config::CONFIG_SINGLESTEP).value_or(false)) { // Single stepping also enforces single instruction size blocks Set(FEXCore::Config::ConfigOption::CONFIG_MAXINST, "1"); } } void AddLayer(fextl::unique_ptr _Layer) { ConfigLayers.emplace(_Layer->GetLayerType(), std::move(_Layer)); } bool Exists(ConfigOption Option) { return Meta->OptionExists(Option); } std::optional All(ConfigOption Option) { return Meta->All(Option); } std::optional Get(ConfigOption Option) { return Meta->Get(Option); } template std::optional GetConv(ConfigOption Option) { return Meta->GetConv(Option); } template std::optional GetConv(ConfigOption Option); template std::optional GetConv(ConfigOption Option); template std::optional GetConv(ConfigOption Option); template std::optional GetConv(ConfigOption Option); template std::optional GetConv(ConfigOption Option); void Set(ConfigOption Option, std::string_view Data) { Meta->Set(Option, Data); } void Erase(ConfigOption Option) { Meta->Erase(Option); } template T Value::GetIfExists(FEXCore::Config::ConfigOption Option, T Default) { auto Value = FEXCore::Config::GetConv(Option); if (Value) { return *Value; } return Default; } template<> fextl::string Value::GetIfExists(FEXCore::Config::ConfigOption Option, fextl::string Default) { auto Value = FEXCore::Config::Get(Option); if (Value) { return **Value; } else { return Default; } } template<> fextl::string Value::GetIfExists(FEXCore::Config::ConfigOption Option, std::string_view Default) { auto Value = FEXCore::Config::Get(Option); if (Value) { return **Value; } else { return fextl::string(Default); } } template bool Value::GetIfExists(FEXCore::Config::ConfigOption Option, bool Default); template int8_t Value::GetIfExists(FEXCore::Config::ConfigOption Option, int8_t Default); template uint8_t Value::GetIfExists(FEXCore::Config::ConfigOption Option, uint8_t Default); template int16_t Value::GetIfExists(FEXCore::Config::ConfigOption Option, int16_t Default); template uint16_t Value::GetIfExists(FEXCore::Config::ConfigOption Option, uint16_t Default); template int32_t Value::GetIfExists(FEXCore::Config::ConfigOption Option, int32_t Default); template uint32_t Value::GetIfExists(FEXCore::Config::ConfigOption Option, uint32_t Default); template int64_t Value::GetIfExists(FEXCore::Config::ConfigOption Option, int64_t Default); template uint64_t Value::GetIfExists(FEXCore::Config::ConfigOption Option, uint64_t Default); // Constructor template Value::Value(FEXCore::Config::ConfigOption _Option, fextl::string Default); template Value::Value(FEXCore::Config::ConfigOption _Option, bool Default); template Value::Value(FEXCore::Config::ConfigOption _Option, uint8_t Default); template Value::Value(FEXCore::Config::ConfigOption _Option, uint64_t Default); template void Value::GetListIfExists(FEXCore::Config::ConfigOption Option, StringArrayType* List) { auto Value = FEXCore::Config::All(Option); List->clear(); if (Value) { *List = **Value; } } template void Value::GetListIfExists(FEXCore::Config::ConfigOption Option, StringArrayType* List); #define CONFIG_AFFECTSCODEGEN #include fextl::string SerializeForCache() { fextl::string Config {}; auto append_string_triple = [](fextl::string& Config, std::string_view Key, ConfigOption Option, auto Value) { Config.append(Key); Config.append(1, '\0'); Config.append(fextl::fmt::format("{}", FEXCore::ToUnderlying(Option))); Config.append(1, '\0'); Config.append(fextl::fmt::format("{}", Value)); Config.append(1, '\0'); }; const auto SerializeValue = [&Config, append_string_triple](auto ConfigVal, const auto Default) { if (!Config_AffectsCodeGen[FEXCore::ToUnderlying(Option)]) { // Skip everything that the config says doesn't affect codegen. return; } append_string_triple(Config, FEXCore::Config::GetConfigJSONName(Option), Option, ConfigVal()); }; #define OPT_BASE(type, group, enum, json, default) \ SerializeValue.template operator()(FEXCore::Config::Get_##enum(), default); #define OPT_STR(group, enum, json, default) \ SerializeValue.template operator()(FEXCore::Config::Get_##enum(), default); #define OPT_STRARRAY(group, enum, json, default) // Unsupported. #define OPT_STRENUM(group, enum, json, default) // Unsupported. #include return Config; } FEX_DEFAULT_VISIBILITY bool CheckConfigMatches(std::string_view Config) { // Serialize current config and just check if it matches. return SerializeForCache() == Config; } } // namespace FEXCore::Config