diff options
| -rwxr-xr-x | build.sh | 5 | ||||
| -rwxr-xr-x | docker/build-inner.sh | 24 | ||||
| -rw-r--r-- | src/src/env.cpp | 78 | ||||
| -rw-r--r-- | src/src/main.cpp | 50 | ||||
| -rw-r--r-- | src/src/nxmhandler_linux.cpp | 81 | ||||
| -rw-r--r-- | src/src/prefixsetuprunner.cpp | 278 | ||||
| -rw-r--r-- | src/src/prefixsetuprunner.h | 1 | ||||
| -rw-r--r-- | src/src/protonlauncher.cpp | 2 |
8 files changed, 509 insertions, 10 deletions
@@ -65,9 +65,8 @@ fi echo "=== Starting build (mode: ${BUILD_MODE}) ===" # BUILD_JOBS controls parallelism (override with `BUILD_JOBS=N ./build.sh`). -# Defaults to all available cores; set to 4 by default for the in-progress -# code-review pass to keep the host responsive. -BUILD_JOBS="${BUILD_JOBS:-4}" +# Defaults to all available cores. +BUILD_JOBS="${BUILD_JOBS:-$(nproc 2>/dev/null || getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1)}" ${DOCKER} run --rm \ -v "${SCRIPT_DIR}:/src:rw" \ -v "${CCACHE_DIR}:/ccache:rw" \ diff --git a/docker/build-inner.sh b/docker/build-inner.sh index 9c05783..3cc5559 100755 --- a/docker/build-inner.sh +++ b/docker/build-inner.sh @@ -39,7 +39,11 @@ cmake -S . -B build -G Ninja \ -DFLUORINE_BUILD_COMMIT="${FLUORINE_BUILD_COMMIT}" \ "${CMAKE_EXTRA_ARGS[@]}" -cmake --build build --parallel "${BUILD_JOBS:-}" +if [ -n "${BUILD_JOBS:-}" ]; then + cmake --build build --parallel "${BUILD_JOBS}" +else + cmake --build build --parallel +fi MODORG_BIN="build/src/src/ModOrganizer" if [ ! -f "${MODORG_BIN}" ]; then @@ -610,6 +614,24 @@ unset PYTHONPATH PYTHONNOUSERSITE PYTHONHOME MO2_PYTHON_DIR export QT_PLUGIN_PATH="${RUN}/qt6plugins" export QT_QPA_PLATFORM_PLUGIN_PATH="${RUN}/qt6plugins/platforms" +# The portable bundle ships its own fontconfig library. If it reads the host's +# newer /etc/fonts configuration, older bundled fontconfig can warn on syntax +# like xsi:nil and newer generic families. Use a minimal compatible config for +# Fluorine itself; launched games/tools restore the user's original env. +mkdir -p "${RUN}/etc/fonts" +cat > "${RUN}/etc/fonts/fonts.conf" <<EOF +<?xml version="1.0"?> +<!DOCTYPE fontconfig SYSTEM "urn:fontconfig:fonts.dtd"> +<fontconfig> + <dir>/usr/share/fonts</dir> + <dir>/usr/local/share/fonts</dir> + <dir prefix="xdg">fonts</dir> + <cachedir prefix="xdg">fontconfig</cachedir> +</fontconfig> +EOF +export FONTCONFIG_FILE="${RUN}/etc/fonts/fonts.conf" +export FONTCONFIG_PATH="${RUN}/etc/fonts" + # Raise open file descriptor limit — large modlists with FUSE VFS # can easily exceed the default 1024 ulimit -n 65536 2>/dev/null diff --git a/src/src/env.cpp b/src/src/env.cpp index e731016..8079f56 100644 --- a/src/src/env.cpp +++ b/src/src/env.cpp @@ -10,6 +10,11 @@ #include <log.h> #include <utility.h> +#include <QCoreApplication> +#include <QDir> +#include <QFileInfo> +#include <QProcess> +#include <QProcessEnvironment> #include <QTimeZone> #include <climits> @@ -22,6 +27,78 @@ #include <iostream> #include <unistd.h> +namespace +{ +QProcessEnvironment hostDesktopEnvironment() +{ + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + auto restoreOrStrip = [&env](const QString& var, const QString& origVar) { + if (env.contains(origVar)) { + const QString orig = env.value(origVar); + if (orig.isEmpty()) { + env.remove(var); + } else { + env.insert(var, orig); + } + env.remove(origVar); + } + }; + + restoreOrStrip(QStringLiteral("LD_LIBRARY_PATH"), + QStringLiteral("FLUORINE_ORIG_LD_LIBRARY_PATH")); + restoreOrStrip(QStringLiteral("LD_PRELOAD"), + QStringLiteral("FLUORINE_ORIG_LD_PRELOAD")); + restoreOrStrip(QStringLiteral("PATH"), QStringLiteral("FLUORINE_ORIG_PATH")); + restoreOrStrip(QStringLiteral("XDG_DATA_DIRS"), + QStringLiteral("FLUORINE_ORIG_XDG_DATA_DIRS")); + restoreOrStrip(QStringLiteral("QT_PLUGIN_PATH"), + QStringLiteral("FLUORINE_ORIG_QT_PLUGIN_PATH")); + + QStringList toolDirs; + auto addToolDir = [&toolDirs](const QString& path) { + if (!path.isEmpty() && QDir(path).exists() && !toolDirs.contains(path)) { + toolDirs.append(path); + } + }; + + const QString baseDir = env.value(QStringLiteral("MO2_BASE_DIR")); + addToolDir(env.value(QStringLiteral("MO2_LIBS_DIR"))); + if (!baseDir.isEmpty()) { + addToolDir(QDir(baseDir).filePath(QStringLiteral("lib"))); + addToolDir(baseDir); + } + const QString appDir = QCoreApplication::applicationDirPath(); + addToolDir(QDir(appDir).filePath(QStringLiteral("lib"))); + addToolDir(appDir); + if (!toolDirs.isEmpty()) { + const QString path = env.value(QStringLiteral("PATH")); + env.insert(QStringLiteral("PATH"), + toolDirs.join(QLatin1Char(':')) + + (path.isEmpty() ? QString() : QStringLiteral(":") + path)); + } + + QString fontDir; + if (!baseDir.isEmpty()) { + fontDir = QDir(baseDir).filePath(QStringLiteral("etc/fonts")); + } + if (fontDir.isEmpty() || + !QFileInfo::exists(QDir(fontDir).filePath(QStringLiteral("fonts.conf")))) { + fontDir = QDir(appDir).filePath(QStringLiteral("etc/fonts")); + } + const QString fontConfig = QDir(fontDir).filePath(QStringLiteral("fonts.conf")); + if (QFileInfo::exists(fontConfig)) { + env.insert(QStringLiteral("FONTCONFIG_FILE"), fontConfig); + env.insert(QStringLiteral("FONTCONFIG_PATH"), fontDir); + } else { + env.remove(QStringLiteral("FONTCONFIG_FILE")); + env.remove(QStringLiteral("FONTCONFIG_PATH")); + } + + env.remove(QStringLiteral("QT_QPA_PLATFORM_PLUGIN_PATH")); + return env; +} +} + namespace env { @@ -315,6 +392,7 @@ Association getAssociation(const QFileInfo& targetInfo) const QString mimeType = "application/x-" + targetInfo.suffix(); QProcess xdgMime; + xdgMime.setProcessEnvironment(hostDesktopEnvironment()); xdgMime.start("xdg-mime", QStringList() << "query" << "default" << mimeType); if (!xdgMime.waitForFinished(3000)) { diff --git a/src/src/main.cpp b/src/src/main.cpp index a7ab578..fc10456 100644 --- a/src/src/main.cpp +++ b/src/src/main.cpp @@ -13,6 +13,9 @@ #include <log.h> #include <report.h> +#include <QDir> +#include <QFile> +#include <QFileInfo> #include <QString> #include <atomic> @@ -29,6 +32,51 @@ using namespace MOBase; int run(int argc, char* argv[]); +namespace +{ +const char* COMPAT_FONTCONFIG = R"(<?xml version="1.0"?> +<!DOCTYPE fontconfig SYSTEM "urn:fontconfig:fonts.dtd"> +<fontconfig> + <dir>/usr/share/fonts</dir> + <dir>/usr/local/share/fonts</dir> + <dir prefix="xdg">fonts</dir> + <cachedir prefix="xdg">fontconfig</cachedir> +</fontconfig> +)"; + +void configureCompatibleFontconfig(int argc, char* argv[]) +{ + if (qEnvironmentVariableIsSet("FLUORINE_DISABLE_FONTCONFIG_FIX")) { + return; + } + + QString appDir = QDir::currentPath(); + if (argc > 0 && argv[0] != nullptr && argv[0][0] != '\0') { + const QFileInfo exeInfo(QString::fromLocal8Bit(argv[0])); + if (exeInfo.exists()) { + appDir = exeInfo.absoluteDir().absolutePath(); + } + } + + const QString fontDir = QDir(appDir).filePath(QStringLiteral("etc/fonts")); + const QString configPath = QDir(fontDir).filePath(QStringLiteral("fonts.conf")); + + if (!QFileInfo::exists(configPath)) { + QDir().mkpath(fontDir); + QFile config(configPath); + if (config.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) { + config.write(COMPAT_FONTCONFIG); + config.close(); + } + } + + if (QFileInfo::exists(configPath)) { + qputenv("FONTCONFIG_FILE", QFile::encodeName(configPath)); + qputenv("FONTCONFIG_PATH", QFile::encodeName(fontDir)); + } +} +} + int main(int argc, char* argv[]) { const int r = run(argc, argv); @@ -38,6 +86,8 @@ int main(int argc, char* argv[]) int run(int argc, char* argv[]) { + configureCompatibleFontconfig(argc, argv); + if (argc >= 3 && QString(argv[1]) == "nxm-handle") { QString nxmUrl = QString::fromLocal8Bit(argv[2]); if (nxmUrl == "nxm-handle" && argc >= 4) { diff --git a/src/src/nxmhandler_linux.cpp b/src/src/nxmhandler_linux.cpp index 520ab55..a30f38d 100644 --- a/src/src/nxmhandler_linux.cpp +++ b/src/src/nxmhandler_linux.cpp @@ -12,6 +12,7 @@ #include <QLocalServer> #include <QLocalSocket> #include <QProcess> +#include <QProcessEnvironment> #include <QTextStream> #include <QUrl> #include <QUrlQuery> @@ -156,7 +157,85 @@ void removeDesktopFile(QStringList& desktopFiles, const QString& desktopFile) void runDesktopCommand(const QString& program, const QStringList& arguments) { - const int result = QProcess::execute(program, arguments); + QProcess proc; + proc.setProgram(program); + proc.setArguments(arguments); + proc.setProcessChannelMode(QProcess::ForwardedChannels); + + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + auto restoreOrStrip = [&env](const QString& var, const QString& origVar) { + if (env.contains(origVar)) { + const QString orig = env.value(origVar); + if (orig.isEmpty()) { + env.remove(var); + } else { + env.insert(var, orig); + } + env.remove(origVar); + } + }; + + restoreOrStrip(QStringLiteral("LD_LIBRARY_PATH"), + QStringLiteral("FLUORINE_ORIG_LD_LIBRARY_PATH")); + restoreOrStrip(QStringLiteral("LD_PRELOAD"), + QStringLiteral("FLUORINE_ORIG_LD_PRELOAD")); + restoreOrStrip(QStringLiteral("PATH"), QStringLiteral("FLUORINE_ORIG_PATH")); + restoreOrStrip(QStringLiteral("XDG_DATA_DIRS"), + QStringLiteral("FLUORINE_ORIG_XDG_DATA_DIRS")); + restoreOrStrip(QStringLiteral("QT_PLUGIN_PATH"), + QStringLiteral("FLUORINE_ORIG_QT_PLUGIN_PATH")); + + QStringList toolDirs; + auto addToolDir = [&toolDirs](const QString& path) { + if (!path.isEmpty() && QDir(path).exists() && !toolDirs.contains(path)) { + toolDirs.append(path); + } + }; + + const QString baseDir = env.value(QStringLiteral("MO2_BASE_DIR")); + addToolDir(env.value(QStringLiteral("MO2_LIBS_DIR"))); + if (!baseDir.isEmpty()) { + addToolDir(QDir(baseDir).filePath(QStringLiteral("lib"))); + addToolDir(baseDir); + } + const QString appDir = QCoreApplication::applicationDirPath(); + addToolDir(QDir(appDir).filePath(QStringLiteral("lib"))); + addToolDir(appDir); + if (!toolDirs.isEmpty()) { + const QString path = env.value(QStringLiteral("PATH")); + env.insert(QStringLiteral("PATH"), + toolDirs.join(QLatin1Char(':')) + + (path.isEmpty() ? QString() : QStringLiteral(":") + path)); + } + + QString fontDir; + if (!baseDir.isEmpty()) { + fontDir = QDir(baseDir).filePath(QStringLiteral("etc/fonts")); + } + if (fontDir.isEmpty() || + !QFileInfo::exists(QDir(fontDir).filePath(QStringLiteral("fonts.conf")))) { + fontDir = QDir(appDir).filePath(QStringLiteral("etc/fonts")); + } + const QString fontConfig = QDir(fontDir).filePath(QStringLiteral("fonts.conf")); + if (QFileInfo::exists(fontConfig)) { + env.insert(QStringLiteral("FONTCONFIG_FILE"), fontConfig); + env.insert(QStringLiteral("FONTCONFIG_PATH"), fontDir); + } else { + env.remove(QStringLiteral("FONTCONFIG_FILE")); + env.remove(QStringLiteral("FONTCONFIG_PATH")); + } + + env.remove(QStringLiteral("QT_QPA_PLATFORM_PLUGIN_PATH")); + proc.setProcessEnvironment(env); + + proc.start(); + if (!proc.waitForStarted()) { + log::debug("{} is not available", program); + return; + } + + proc.waitForFinished(); + const int result = proc.exitCode(); if (result == -2) { log::debug("{} is not available", program); } else if (result != 0) { diff --git a/src/src/prefixsetuprunner.cpp b/src/src/prefixsetuprunner.cpp index 0c49baa..f599a30 100644 --- a/src/src/prefixsetuprunner.cpp +++ b/src/src/prefixsetuprunner.cpp @@ -31,6 +31,16 @@ #include <csignal> #include <sys/types.h> +namespace +{ +QString linuxPathToWineZPath(const QString& path) +{ + QString clean = QDir::cleanPath(QFileInfo(path).absoluteFilePath()); + clean.replace(QLatin1Char('/'), QLatin1Char('\\')); + return QStringLiteral("Z:") + clean; +} +} + // ============================================================================ // Constants // ============================================================================ @@ -53,6 +63,113 @@ static const char* DOTNET10_SDK_URL = "https://builds.dotnet.microsoft.com/dotnet/Sdk/10.0.201/" "dotnet-sdk-10.0.201-win-x64.exe"; +static const char* NUGET_CONFIG_TEMPLATE = R"(<?xml version="1.0" encoding="utf-8"?> +<configuration> + <config> + <add key="signatureValidationMode" value="accept" /> + </config> + <packageSources> + <add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" /> + </packageSources> + <trustedSigners> + <repository name="nuget.org" serviceIndex="https://api.nuget.org/v3/index.json"> + <certificate fingerprint="0E5F38F57DC1BCC806D8494F4F90FBCEDD988B46760709CBEEC6F4219AA6157D" hashAlgorithm="SHA256" allowUntrustedRoot="true" /> + <certificate fingerprint="5A2901D6ADA3D18260B9C6DFE2133C95D74B9EEF6AE0E5DC334C8454D1477DF4" hashAlgorithm="SHA256" allowUntrustedRoot="true" /> + <certificate fingerprint="1F4B311D9ACC115C8DC8018B5A49E00FCE6DA8E2855F9F014CA6F34570BC482D" hashAlgorithm="SHA256" allowUntrustedRoot="true" /> + </repository> + </trustedSigners> +</configuration> +)"; + +static const char* CERT_IMPORTER_CSPROJ = R"(<?xml version="1.0" encoding="utf-8"?> +<Project Sdk="Microsoft.NET.Sdk"> + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>net10.0</TargetFramework> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + </PropertyGroup> +</Project> +)"; + +static const char* CERT_IMPORTER_PROGRAM = R"(using System.Security.Cryptography.X509Certificates; + +if (args.Length % 2 != 0) +{ + Console.Error.WriteLine("Expected pairs: <store> <pem-bundle>"); + return 2; +} + +for (var i = 0; i < args.Length; i += 2) +{ + ImportBundle(args[i], args[i + 1]); +} + +return 0; + +static void ImportBundle(string storeName, string bundlePath) +{ + if (!File.Exists(bundlePath)) + { + Console.WriteLine($"Missing bundle: {bundlePath}"); + return; + } + + using var store = new X509Store(storeName, StoreLocation.CurrentUser); + store.Open(OpenFlags.ReadWrite); + + var imported = 0; + foreach (var cert in ReadPemBundle(bundlePath)) + { + var existing = store.Certificates.Find( + X509FindType.FindByThumbprint, cert.Thumbprint, validOnly: false); + if (existing.Count > 0) + { + cert.Dispose(); + continue; + } + + store.Add(cert); + cert.Dispose(); + ++imported; + } + + Console.WriteLine($"Imported {imported} certificate(s) into {storeName}"); +} + +static IEnumerable<X509Certificate2> ReadPemBundle(string bundlePath) +{ + var text = File.ReadAllText(bundlePath); + const string begin = "-----BEGIN CERTIFICATE-----"; + const string end = "-----END CERTIFICATE-----"; + var offset = 0; + + while (true) + { + var beginIndex = text.IndexOf(begin, offset, StringComparison.Ordinal); + if (beginIndex < 0) + { + yield break; + } + + beginIndex += begin.Length; + var endIndex = text.IndexOf(end, beginIndex, StringComparison.Ordinal); + if (endIndex < 0) + { + yield break; + } + + var base64 = text.Substring(beginIndex, endIndex - beginIndex) + .Replace("\r", "") + .Replace("\n", "") + .Trim(); + + yield return X509CertificateLoader.LoadCertificate(Convert.FromBase64String(base64)); + offset = endIndex + end.Length; + } +} +)"; + // d3dcompiler_47: prebuilt DLLs from Mozilla's fxc2 repo (Windows 8.1 SDK redist). static const char* D3DCOMPILER_47_32_URL = "https://github.com/mozilla/fxc2/raw/master/dll/d3dcompiler_47_32.dll"; @@ -576,6 +693,8 @@ void PrefixSetupRunner::buildStepList() [this] { return stepDotNetRuntimes(); }); addStep("dotnet10_sdk", ".NET 10 SDK", [this] { return stepDotNetInstall(DOTNET10_SDK_URL, ".NET 10 SDK"); }); + addStep("nuget_signature_policy", "NuGet Signature Policy", + [this] { return stepNuGetSignaturePolicy(); }); addStep("game_detection", "Auto-Detect Games", [this] { return stepGameDetection(); }); @@ -784,7 +903,8 @@ QProcess* PrefixSetupRunner::buildWrappedProcess( // container's PATH. if (QDir(xrandrDir).exists()) { slrArgs << "--" << QStringLiteral("/usr/bin/env") - << QStringLiteral("PATH=%1:/usr/bin:/bin").arg(xrandrDir) << exe; + << QStringLiteral("PATH=%1:/usr/bin:/bin").arg(xrandrDir); + slrArgs << exe; } else { slrArgs << "--" << exe; } @@ -982,7 +1102,7 @@ bool PrefixSetupRunner::stepProtonInit() // Keep DISPLAY/WAYLAND_DISPLAY from the host: Proton-GE protonfixes runs // `xrandr` during wineboot -u to detect monitors, and xrandr exits 1 if // it can't open a display, which cascades to a failed prefix init on - // newer Proton-GE builds. wineboot -u is unattended and doesn't pop UI. + // newer Proton-GE builds. env["WINEDLLOVERRIDES"] = "msdia80.dll=n;conhost.exe=d;cmd.exe=d"; // ntsync on kernel 7.0+ can deadlock wineboot -u during prefix init // under Proton 11 (wineboot blocks in ntsync_char_ioctl forever). Force @@ -992,15 +1112,15 @@ bool PrefixSetupRunner::stepProtonInit() env["PROTON_NO_NTSYNC"] = "1"; env["WINENTSYNC"] = "0"; - emit logMessage("Initializing Wine prefix with Proton..."); - - QByteArray protonOutput; // waitforexitandrun (not "run") is required for Proton 11+ which uses // use_sessions=1 — a plain "run" forks into a session manager that never // exits, hanging prefix init. + QByteArray protonOutput; + emit logMessage("Initializing Wine prefix with Proton..."); const int rc = runProcess(protonScript, {"waitforexitandrun", "wineboot", "-u"}, env, -1, &protonOutput); + if (rc != 0) { // Detect a broken Proton install: setup_prefix copies DLLs from Proton's // bundled default_pfx template, so a FileNotFoundError there means the @@ -1600,6 +1720,154 @@ bool PrefixSetupRunner::stepDotNetInstall(const QString& url, const QString& nam return true; } +bool PrefixSetupRunner::stepNuGetSignaturePolicy() +{ + emit logMessage("Configuring NuGet signature policy..."); + + const QString usersRoot = QDir(m_prefixPath).filePath("drive_c/users"); + QDir usersDir(usersRoot); + if (!usersDir.exists()) { + if (!usersDir.mkpath(QStringLiteral("."))) { + currentStep().errorMessage = "Failed to create Wine users directory"; + return false; + } + } + + QStringList userNames = + usersDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::Readable); + userNames.removeAll(QStringLiteral("Public")); + userNames.removeAll(QStringLiteral("Default")); + userNames.removeAll(QStringLiteral("Default User")); + + if (userNames.isEmpty()) { + userNames << QStringLiteral("steamuser"); + } + + int written = 0; + for (const QString& userName : userNames) { + if (isCancelled()) { + return false; + } + + const QString nugetDir = usersDir.filePath( + userName + QStringLiteral("/AppData/Roaming/NuGet")); + if (!QDir().mkpath(nugetDir)) { + emit logMessage(QStringLiteral("Skipping NuGet config for %1: cannot create %2") + .arg(userName, nugetDir)); + continue; + } + + const QString configPath = QDir(nugetDir).filePath(QStringLiteral("NuGet.Config")); + if (QFileInfo::exists(configPath)) { + emit logMessage(QStringLiteral( + "NuGet.Config already exists for %1; leaving user config unchanged.") + .arg(userName)); + continue; + } + + QFile config(configPath); + if (!config.open(QIODevice::WriteOnly | QIODevice::Text)) { + emit logMessage(QStringLiteral("Skipping NuGet config for %1: cannot write %2") + .arg(userName, configPath)); + continue; + } + config.write(NUGET_CONFIG_TEMPLATE); + config.close(); + ++written; + } + + const QString dotnetRoot = + QDir(m_prefixPath).filePath("drive_c/Program Files/dotnet"); + const QString dotnetExe = QDir(dotnetRoot).filePath(QStringLiteral("dotnet.exe")); + if (!QFileInfo::exists(dotnetExe)) { + currentStep().errorMessage = ".NET SDK install did not provide dotnet.exe"; + return false; + } + + const QString dotnetSdkRoot = QDir(dotnetRoot).filePath(QStringLiteral("sdk")); + QDir sdkDir(dotnetSdkRoot); + const QStringList sdkVersions = + sdkDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name); + QString codeRoots; + QString timestampRoots; + for (auto it = sdkVersions.crbegin(); it != sdkVersions.crend(); ++it) { + const QString trustedRoots = + sdkDir.filePath(*it + QStringLiteral("/trustedroots")); + const QString candidateCodeRoots = + QDir(trustedRoots).filePath(QStringLiteral("codesignctl.pem")); + const QString candidateTimestampRoots = + QDir(trustedRoots).filePath(QStringLiteral("timestampctl.pem")); + if (QFileInfo::exists(candidateCodeRoots) && + QFileInfo::exists(candidateTimestampRoots)) { + codeRoots = candidateCodeRoots; + timestampRoots = candidateTimestampRoots; + emit logMessage(QStringLiteral("Found .NET trusted root bundles in SDK %1").arg(*it)); + break; + } + } + + if (codeRoots.isEmpty() || timestampRoots.isEmpty()) { + currentStep().errorMessage = ".NET SDK trusted root bundles were not found"; + return false; + } + + const QString importerDir = QDir(fluorineTmpDir()).filePath( + QStringLiteral("nuget-cert-importer")); + QDir(importerDir).removeRecursively(); + if (!QDir().mkpath(importerDir)) { + currentStep().errorMessage = "Failed to create certificate importer workspace"; + return false; + } + + QFile projectFile(QDir(importerDir).filePath(QStringLiteral("FluorineCertImport.csproj"))); + if (!projectFile.open(QIODevice::WriteOnly | QIODevice::Text)) { + currentStep().errorMessage = "Failed to write certificate importer project"; + return false; + } + projectFile.write(CERT_IMPORTER_CSPROJ); + projectFile.close(); + + QFile programFile(QDir(importerDir).filePath(QStringLiteral("Program.cs"))); + if (!programFile.open(QIODevice::WriteOnly | QIODevice::Text)) { + currentStep().errorMessage = "Failed to write certificate importer program"; + return false; + } + programFile.write(CERT_IMPORTER_PROGRAM); + programFile.close(); + + QMap<QString, QString> env = baseWineEnv(); + env["DOTNET_CLI_TELEMETRY_OPTOUT"] = "1"; + env["DOTNET_NOLOGO"] = "1"; + env["DOTNET_SKIP_FIRST_TIME_EXPERIENCE"] = "1"; + env["NUGET_XMLDOC_MODE"] = "skip"; + env["NUGET_CERT_REVOCATION_MODE"] = "offline"; + env["NUGET_EXPERIMENTAL_CHAIN_BUILD_RETRY_POLICY"] = "10,1000"; + env["WINEDLLOVERRIDES"] = "mshtml=d"; + + emit logMessage("Importing .NET code-signing and timestamp roots into Wine..."); + const QString wineImporterDir = linuxPathToWineZPath(importerDir); + const QString wineCodeRoots = linuxPathToWineZPath(codeRoots); + const QString wineTimestampRoots = linuxPathToWineZPath(timestampRoots); + const int rc = runProcess(m_wineBin, + {dotnetExe, QStringLiteral("run"), + QStringLiteral("--project"), wineImporterDir, + QStringLiteral("-p:UseSharedCompilation=false"), + QStringLiteral("--"), + QStringLiteral("Root"), wineCodeRoots, + QStringLiteral("Root"), wineTimestampRoots}, + env); + QDir(importerDir).removeRecursively(); + if (rc != 0) { + currentStep().errorMessage = + QStringLiteral("NuGet certificate import failed (exit code %1)").arg(rc); + return false; + } + + emit logMessage(QStringLiteral("NuGet signature policy configured for %1 Wine user(s).") + .arg(written)); + return true; +} + bool PrefixSetupRunner::stepGameDetection() { emit logMessage("Auto-detecting installed games..."); diff --git a/src/src/prefixsetuprunner.h b/src/src/prefixsetuprunner.h index 0dc5768..aed0102 100644 --- a/src/src/prefixsetuprunner.h +++ b/src/src/prefixsetuprunner.h @@ -108,6 +108,7 @@ private: const QString& name, const QStringList& knownSha25632 = {}, const QStringList& knownSha25664 = {}); + bool stepNuGetSignaturePolicy(); bool stepGameDetection(); bool stepWineRegistry(); bool stepWin11Mode(); diff --git a/src/src/protonlauncher.cpp b/src/src/protonlauncher.cpp index b4ed15b..800d837 100644 --- a/src/src/protonlauncher.cpp +++ b/src/src/protonlauncher.cpp @@ -912,6 +912,8 @@ bool ProtonLauncher::launchWithProton(qint64& pid) const env.insert("DOTNET_ROOT", ""); env.insert("DOTNET_MULTILEVEL_LOOKUP", "0"); + env.insert("NUGET_EXPERIMENTAL_CHAIN_BUILD_RETRY_POLICY", "10,1000"); + env.insert("NUGET_CERT_REVOCATION_MODE", "offline"); // Detect ntsync availability: newer Proton/Wine builds prefer the // in-kernel ntsync driver for synchronization primitives, but it's |
