aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-06-08 17:16:13 -0500
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-06-08 17:17:12 -0500
commitd453ed91b6dc332fd45987c642e9d230efe870c8 (patch)
tree4a9e6d647e912580d78105682941f704099f1167
parentdd3a3a356f46fa064b60cd7e000c7bc5633430b7 (diff)
Fix NuGet cert setup and fontconfig env
-rwxr-xr-xbuild.sh5
-rwxr-xr-xdocker/build-inner.sh24
-rw-r--r--src/src/env.cpp78
-rw-r--r--src/src/main.cpp50
-rw-r--r--src/src/nxmhandler_linux.cpp81
-rw-r--r--src/src/prefixsetuprunner.cpp278
-rw-r--r--src/src/prefixsetuprunner.h1
-rw-r--r--src/src/protonlauncher.cpp2
8 files changed, 509 insertions, 10 deletions
diff --git a/build.sh b/build.sh
index b57d296..bd23ccf 100755
--- a/build.sh
+++ b/build.sh
@@ -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