-
- @GetHubInstallButtonText(context, installState)
-
+
+
+
+ @GetHubInstallButtonText(context, installState)
+
+
+
@if (installState.Installed)
{
QuasarUiPluginCatalog.SafeModeMarkerPath;
@@ -488,6 +556,10 @@
private bool HubOperationInProgress => !string.IsNullOrWhiteSpace(_hubBusyCatalogId);
+ private bool CanBuildUiPlugins => _dotNetSdkStatus.CanBuildUiPlugins;
+
+ private bool DotNetSdkInstallerBusy => _dotNetSdkInstallerBusy || HubCatalog.DotNetSdkInstallInProgress;
+
private IReadOnlyDictionary PluginsPageParameters => new Dictionary
{
["UiPlugins"] = PluginCatalog,
@@ -499,6 +571,7 @@
protected override void OnInitialized()
{
+ _dotNetSdkStatus = HubCatalog.GetDotNetSdkStatus();
HubCatalog.Changed += HandleHubCatalogChanged;
PluginStates.Changed += HandlePluginStatesChanged;
}
@@ -591,6 +664,9 @@
if (HubOperationInProgress)
return;
+ if (!EnsureDotNetSdkForPluginBuild())
+ return;
+
_hubBusyCatalogId = entry.CatalogId;
try
{
@@ -608,6 +684,50 @@
}
}
+ private async Task InstallDotNetSdkAsync()
+ {
+ if (DotNetSdkInstallerBusy)
+ return;
+
+ var availability = HubCatalog.GetDotNetSdkInstallAvailability();
+ if (!availability.CanInstall)
+ {
+ Snackbar.Add(availability.Message, Severity.Warning);
+ return;
+ }
+
+ var confirmed = await DialogService.ShowMessageBoxAsync(
+ "Install .NET SDK?",
+ "Quasar will run install.sh to install the .NET SDK required for source-built UI plugins. This may install system packages and can fail if sudo needs a password.",
+ yesText: "Install SDK",
+ cancelText: "Cancel");
+ if (confirmed != true)
+ return;
+
+ _dotNetSdkInstallerBusy = true;
+ _lastDotNetSdkInstallResult = null;
+ try
+ {
+ var result = await HubCatalog.InstallDotNetSdkAsync();
+ _lastDotNetSdkInstallResult = result;
+ _dotNetSdkStatus = HubCatalog.GetDotNetSdkStatus();
+ Snackbar.Add(result.Message, result.Succeeded ? Severity.Success : Severity.Error);
+ }
+ catch (Exception exception)
+ {
+ _lastDotNetSdkInstallResult = new QuasarDotNetSdkInstallResult(
+ Succeeded: false,
+ exception.Message,
+ string.Empty);
+ Snackbar.Add(exception.Message, Severity.Error);
+ }
+ finally
+ {
+ _dotNetSdkInstallerBusy = false;
+ await InvokeAsync(StateHasChanged);
+ }
+ }
+
private async Task RemoveHubPluginAsync(QuasarUiPluginHubEntry entry)
{
if (HubOperationInProgress)
@@ -710,11 +830,32 @@
string.Equals(_hubBusyCatalogId, entry.CatalogId, StringComparison.OrdinalIgnoreCase);
private bool IsHubInstallDisabled(QuasarUiPluginHubEntry entry) =>
+ !CanBuildUiPlugins ||
HubOperationInProgress ||
_hubRefreshing ||
string.IsNullOrWhiteSpace(entry.RepoId) ||
string.IsNullOrWhiteSpace(entry.Commit);
+ private string GetHubInstallDisabledReason(QuasarUiPluginHubEntry entry)
+ {
+ if (!CanBuildUiPlugins)
+ return $".NET SDK required to build UI plugins. Install .NET {_dotNetSdkStatus.RequiredMajorVersion} SDK.";
+
+ if (HubOperationInProgress)
+ return "Another QuasarHub operation is running.";
+
+ if (_hubRefreshing)
+ return "QuasarHub catalog refresh is running.";
+
+ if (string.IsNullOrWhiteSpace(entry.RepoId))
+ return "QuasarHub entry has no repository.";
+
+ if (string.IsNullOrWhiteSpace(entry.Commit))
+ return "QuasarHub entry has no pinned commit.";
+
+ return string.Empty;
+ }
+
private string GetHubInstallButtonText(QuasarUiPluginHubEntry entry, QuasarUiPluginInstallState installState)
{
if (IsHubBusy(entry))
@@ -786,6 +927,36 @@
private static string GetHubEnableButtonIcon(QuasarUiPluginInstallState installState) =>
installState.Enabled ? Icons.Material.Filled.VisibilityOff : Icons.Material.Filled.Visibility;
+ private Color GetDotNetSdkChipColor() =>
+ CanBuildUiPlugins ? Color.Success : Color.Warning;
+
+ private string GetDotNetSdkChipText() =>
+ CanBuildUiPlugins ? $".NET {_dotNetSdkStatus.RequiredMajorVersion} SDK ready" : ".NET SDK missing";
+
+ private void RefreshDotNetSdkStatus()
+ {
+ _dotNetSdkStatus = HubCatalog.RefreshDotNetSdkStatus();
+ Snackbar.Add(_dotNetSdkStatus.Message, CanBuildUiPlugins ? Severity.Success : Severity.Warning);
+ }
+
+ private bool EnsureDotNetSdkForPluginBuild()
+ {
+ _dotNetSdkStatus = HubCatalog.RefreshDotNetSdkStatus();
+ if (CanBuildUiPlugins)
+ return true;
+
+ Snackbar.Add(_dotNetSdkStatus.Message, Severity.Warning);
+ return false;
+ }
+
+ private static string TrimInstallerOutput(string output)
+ {
+ const int maxCharacters = 8000;
+ return output.Length <= maxCharacters
+ ? output
+ : output[^maxCharacters..];
+ }
+
private static string GetHubDisplayName(QuasarUiPluginHubEntry entry) =>
string.IsNullOrWhiteSpace(entry.FriendlyName) ? entry.CatalogId : entry.FriendlyName;
@@ -839,6 +1010,7 @@
private void HandleHubCatalogChanged()
{
+ _dotNetSdkStatus = HubCatalog.GetDotNetSdkStatus();
_ = InvokeAsync(StateHasChanged);
}
diff --git a/Quasar/Services/Plugins/QuasarUiPluginHubCatalogService.cs b/Quasar/Services/Plugins/QuasarUiPluginHubCatalogService.cs
index a8539eea..cbd5065d 100644
--- a/Quasar/Services/Plugins/QuasarUiPluginHubCatalogService.cs
+++ b/Quasar/Services/Plugins/QuasarUiPluginHubCatalogService.cs
@@ -18,6 +18,7 @@ public sealed class QuasarUiPluginHubCatalogService
private const string PluginAbstractionsAssemblyFileName = "Quasar.Plugin.Abstractions.dll";
private const string MagnetarProtocolAssemblyFileName = "Magnetar.Protocol.dll";
private const string CompanionOutputRelativeDirectory = ".quasar/companions";
+ private static readonly int RequiredDotNetSdkMajor = Environment.Version.Major;
public const string DefaultHubName = "QuasarHub";
public const string DefaultHubRepo = "CometWorks/quasar-hub";
@@ -36,6 +37,9 @@ public sealed class QuasarUiPluginHubCatalogService
private readonly IHostEnvironment _environment;
private readonly QuasarUiPluginStateStore _pluginStates;
private readonly QuasarUiPluginCatalog _uiPluginCatalog;
+ private readonly SemaphoreSlim _dotNetSdkInstallLock = new(1, 1);
+ private QuasarDotNetSdkStatus _dotNetSdkStatus;
+ private bool _dotNetSdkInstallInProgress;
private List _entries;
public QuasarUiPluginHubCatalogService(
@@ -52,6 +56,7 @@ public QuasarUiPluginHubCatalogService(
_environment = environment;
_pluginStates = pluginStates;
_uiPluginCatalog = uiPluginCatalog;
+ _dotNetSdkStatus = DetectDotNetSdkStatus();
_entries = LoadCache();
}
@@ -61,6 +66,15 @@ public QuasarUiPluginHubCatalogService(
public string LastError { get; private set; } = string.Empty;
+ public bool DotNetSdkInstallInProgress
+ {
+ get
+ {
+ lock (_sync)
+ return _dotNetSdkInstallInProgress;
+ }
+ }
+
public IReadOnlyList GetEntries()
{
lock (_sync)
@@ -74,6 +88,130 @@ public IReadOnlyList GetEntries()
}
}
+ public QuasarDotNetSdkStatus GetDotNetSdkStatus()
+ {
+ lock (_sync)
+ return _dotNetSdkStatus;
+ }
+
+ public QuasarDotNetSdkStatus RefreshDotNetSdkStatus()
+ {
+ var status = DetectDotNetSdkStatus();
+ lock (_sync)
+ _dotNetSdkStatus = status;
+
+ Changed?.Invoke();
+ return status;
+ }
+
+ public QuasarDotNetSdkInstallAvailability GetDotNetSdkInstallAvailability()
+ {
+ if (!OperatingSystem.IsLinux())
+ {
+ return new QuasarDotNetSdkInstallAvailability(
+ CanInstall: false,
+ "Automatic SDK install is only available on Linux through install.sh.",
+ string.Empty);
+ }
+
+ var scriptPath = GetInstallScriptPath();
+ if (string.IsNullOrWhiteSpace(scriptPath))
+ {
+ return new QuasarDotNetSdkInstallAvailability(
+ CanInstall: false,
+ "install.sh was not found beside Quasar. Install the .NET SDK manually, then refresh the SDK check.",
+ string.Empty);
+ }
+
+ return new QuasarDotNetSdkInstallAvailability(
+ CanInstall: true,
+ "Run install.sh to install the .NET SDK required for source-built QuasarHub UI plugins.",
+ scriptPath);
+ }
+
+ public async Task InstallDotNetSdkAsync(CancellationToken cancellationToken = default)
+ {
+ var sdkStatus = RefreshDotNetSdkStatus();
+ if (sdkStatus.CanBuildUiPlugins)
+ {
+ return new QuasarDotNetSdkInstallResult(
+ Succeeded: true,
+ sdkStatus.Message,
+ string.Empty);
+ }
+
+ var availability = GetDotNetSdkInstallAvailability();
+ if (!availability.CanInstall)
+ {
+ return new QuasarDotNetSdkInstallResult(
+ Succeeded: false,
+ availability.Message,
+ string.Empty);
+ }
+
+ if (!await _dotNetSdkInstallLock.WaitAsync(0, cancellationToken))
+ {
+ return new QuasarDotNetSdkInstallResult(
+ Succeeded: false,
+ "SDK install is already running.",
+ string.Empty);
+ }
+
+ SetDotNetSdkInstallInProgress(true);
+ try
+ {
+ _logger.LogInformation(
+ "Running {ScriptPath} to install the .NET SDK for QuasarHub UI plugin builds.",
+ availability.InstallScriptPath);
+
+ var scriptDirectory = Path.GetDirectoryName(availability.InstallScriptPath);
+ var startInfo = new ProcessStartInfo("bash")
+ {
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ WorkingDirectory = string.IsNullOrWhiteSpace(scriptDirectory) ? AppContext.BaseDirectory : scriptDirectory,
+ };
+ startInfo.ArgumentList.Add(availability.InstallScriptPath);
+ startInfo.ArgumentList.Add("--install-ui-plugin-sdk-only");
+ startInfo.ArgumentList.Add("--yes");
+
+ var processResult = await RunProcessCaptureAsync(
+ startInfo,
+ "install.sh --install-ui-plugin-sdk-only",
+ cancellationToken);
+ var refreshedStatus = RefreshDotNetSdkStatus();
+ if (processResult.ExitCode == 0 && refreshedStatus.CanBuildUiPlugins)
+ {
+ return new QuasarDotNetSdkInstallResult(
+ Succeeded: true,
+ refreshedStatus.Message,
+ processResult.Output);
+ }
+
+ var message = processResult.ExitCode == 0
+ ? refreshedStatus.Message
+ : $"SDK install script failed with exit code {processResult.ExitCode}.";
+ return new QuasarDotNetSdkInstallResult(
+ Succeeded: false,
+ message,
+ processResult.Output);
+ }
+ catch (Exception exception)
+ {
+ _logger.LogWarning(exception, "Failed to run install.sh for .NET SDK installation.");
+ return new QuasarDotNetSdkInstallResult(
+ Succeeded: false,
+ exception.Message,
+ string.Empty);
+ }
+ finally
+ {
+ SetDotNetSdkInstallInProgress(false);
+ _dotNetSdkInstallLock.Release();
+ }
+ }
+
public async Task RefreshAsync(CancellationToken cancellationToken = default)
{
var entries = new Dictionary(StringComparer.OrdinalIgnoreCase);
@@ -209,6 +347,10 @@ public async Task InstallOrUpdateAsync(
CancellationToken cancellationToken = default,
bool enableAfterInstall = true)
{
+ var sdkStatus = RefreshDotNetSdkStatus();
+ if (!sdkStatus.CanBuildUiPlugins)
+ throw new InvalidOperationException(sdkStatus.Message);
+
if (string.IsNullOrWhiteSpace(entry.RepoId))
throw new InvalidOperationException("QuasarHub entry has no repository.");
@@ -571,6 +713,24 @@ private static async Task RunProcessAsync(ProcessStartInfo startInfo, string lab
}
}
+ private static async Task RunProcessCaptureAsync(
+ ProcessStartInfo startInfo,
+ string label,
+ CancellationToken cancellationToken)
+ {
+ using var process = Process.Start(startInfo)
+ ?? throw new InvalidOperationException($"Failed to start {label}.");
+ var stdoutTask = process.StandardOutput.ReadToEndAsync(cancellationToken);
+ var stderrTask = process.StandardError.ReadToEndAsync(cancellationToken);
+ await process.WaitForExitAsync(cancellationToken);
+
+ var stdout = await stdoutTask;
+ var stderr = await stderrTask;
+ var output = string.Join(Environment.NewLine, [stdout.Trim(), stderr.Trim()])
+ .Trim();
+ return new ProcessCaptureResult(process.ExitCode, output);
+ }
+
private string GetBuildConfiguration() =>
Environment.GetEnvironmentVariable("QUASAR_UI_PLUGIN_BUILD_CONFIGURATION")
?? _configuration["Quasar:Plugins:BuildConfiguration"]
@@ -761,6 +921,121 @@ private static void TryDeleteDirectory(string path)
}
}
+ private static QuasarDotNetSdkStatus DetectDotNetSdkStatus()
+ {
+ var startInfo = new ProcessStartInfo("dotnet")
+ {
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ };
+ startInfo.ArgumentList.Add("--list-sdks");
+
+ try
+ {
+ using var process = Process.Start(startInfo);
+ if (process is null)
+ return CreateMissingSdkStatus("Failed to start dotnet --list-sdks.");
+
+ if (!process.WaitForExit(5000))
+ {
+ TryKillProcess(process);
+ return CreateMissingSdkStatus("dotnet --list-sdks did not finish within 5 seconds.");
+ }
+
+ var stdout = process.StandardOutput.ReadToEnd();
+ var stderr = process.StandardError.ReadToEnd();
+ if (process.ExitCode != 0)
+ {
+ var output = string.Join(Environment.NewLine, [stdout.Trim(), stderr.Trim()]).Trim();
+ return CreateMissingSdkStatus(string.IsNullOrWhiteSpace(output)
+ ? $"dotnet --list-sdks failed with exit code {process.ExitCode}."
+ : output);
+ }
+
+ var sdkVersions = stdout
+ .Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
+ .Select(GetSdkVersion)
+ .Where(version => !string.IsNullOrWhiteSpace(version))
+ .ToArray();
+ var requiredSdk = sdkVersions.FirstOrDefault(version =>
+ version.StartsWith($"{RequiredDotNetSdkMajor}.", StringComparison.OrdinalIgnoreCase));
+
+ if (!string.IsNullOrWhiteSpace(requiredSdk))
+ {
+ return new QuasarDotNetSdkStatus(
+ RequiredDotNetSdkMajor,
+ DotNetOnPath: true,
+ RequiredSdkAvailable: true,
+ sdkVersions,
+ $".NET {RequiredDotNetSdkMajor} SDK {requiredSdk} is available for Quasar UI plugin builds.");
+ }
+
+ var installed = sdkVersions.Length == 0 ? "none" : string.Join(", ", sdkVersions);
+ return new QuasarDotNetSdkStatus(
+ RequiredDotNetSdkMajor,
+ DotNetOnPath: true,
+ RequiredSdkAvailable: false,
+ sdkVersions,
+ $".NET {RequiredDotNetSdkMajor} SDK is required to build Quasar UI plugins. Installed SDKs: {installed}.");
+ }
+ catch (Exception exception)
+ {
+ return CreateMissingSdkStatus(
+ $"dotnet command was not found or could not be started. .NET {RequiredDotNetSdkMajor} SDK is required to build Quasar UI plugins. {exception.Message}");
+ }
+ }
+
+ private static QuasarDotNetSdkStatus CreateMissingSdkStatus(string message) =>
+ new(
+ RequiredDotNetSdkMajor,
+ DotNetOnPath: false,
+ RequiredSdkAvailable: false,
+ [],
+ message);
+
+ private void SetDotNetSdkInstallInProgress(bool inProgress)
+ {
+ lock (_sync)
+ _dotNetSdkInstallInProgress = inProgress;
+
+ Changed?.Invoke();
+ }
+
+ private string GetInstallScriptPath()
+ {
+ var candidates = new[]
+ {
+ Path.Combine(AppContext.BaseDirectory, "install.sh"),
+ Path.Combine(_environment.ContentRootPath, "install.sh"),
+ Path.Combine(_environment.ContentRootPath, "..", "install.sh"),
+ Path.Combine(Directory.GetCurrentDirectory(), "install.sh"),
+ };
+
+ return candidates
+ .Select(Path.GetFullPath)
+ .FirstOrDefault(File.Exists)
+ ?? string.Empty;
+ }
+
+ private static string GetSdkVersion(string sdkLine)
+ {
+ var index = sdkLine.IndexOf(' ', StringComparison.Ordinal);
+ return index < 0 ? sdkLine : sdkLine[..index];
+ }
+
+ private static void TryKillProcess(Process process)
+ {
+ try
+ {
+ if (!process.HasExited)
+ process.Kill(entireProcessTree: false);
+ }
+ catch
+ {
+ }
+ }
+
private static string GetPluginRootDirectory() =>
Path.Combine(MagnetarPaths.GetQuasarDirectory(), "Plugins");
@@ -803,6 +1078,28 @@ private sealed class QuasarUiPluginHubCatalogCache
public List Entries { get; set; } = [];
}
+
+ private sealed record ProcessCaptureResult(int ExitCode, string Output);
+}
+
+public sealed record QuasarDotNetSdkInstallAvailability(
+ bool CanInstall,
+ string Message,
+ string InstallScriptPath);
+
+public sealed record QuasarDotNetSdkInstallResult(
+ bool Succeeded,
+ string Message,
+ string Output);
+
+public sealed record QuasarDotNetSdkStatus(
+ int RequiredMajorVersion,
+ bool DotNetOnPath,
+ bool RequiredSdkAvailable,
+ IReadOnlyList InstalledSdkVersions,
+ string Message)
+{
+ public bool CanBuildUiPlugins => DotNetOnPath && RequiredSdkAvailable;
}
public sealed class QuasarUiPluginHubEntry
diff --git a/install.sh b/install.sh
index 7307dbee..2c9ff33d 100755
--- a/install.sh
+++ b/install.sh
@@ -14,6 +14,9 @@ START_SERVICE=false
SKIP_BUILD=false
INSTALL_MODE="user"
INSTALL_RENICE_HELPER=false
+INSTALL_UI_PLUGIN_SDK=false
+INSTALL_UI_PLUGIN_SDK_ONLY=false
+ASSUME_YES=false
RENICE_HELPER_PATH="/usr/local/bin/quasar-renice"
RUN_USER="${USER:-}"
RUN_USER_EXPLICIT=false
@@ -32,7 +35,9 @@ For machine-wide service installation, pass --system and run with sudo.
If the required .NET 10 SDK/runtime is missing, the installer detects apt, dnf,
yum, pacman, or zypper, prints the exact install commands, and prompts before
running them. On Debian 13, apt installs first add Microsoft's Debian 13 package
-feed.
+feed. Packaged installs need only the runtime to run Quasar; pass
+--install-ui-plugin-sdk to also install the SDK used for source-built QuasarHub
+UI plugin installs and updates.
Options:
--install-dir Install directory (default: extracted installer root; source installs use /.local/share/Quasar)
@@ -47,6 +52,10 @@ Options:
--no-enable Do not enable the service at boot
--start Restart/start the service after installing
--no-build Install from an existing publish directory
+ --install-ui-plugin-sdk Install .NET SDK for QuasarHub UI plugin builds
+ --install-ui-plugin-sdk-only
+ Only install .NET SDK for QuasarHub UI plugin builds, then exit
+ --yes Run package install commands without confirmation
-h, --help Show this help
EOF
}
@@ -61,7 +70,11 @@ privileged_prefix() {
fi
if have sudo; then
- printf 'sudo '
+ if [[ "$ASSUME_YES" == "true" ]]; then
+ printf 'sudo -n '
+ else
+ printf 'sudo '
+ fi
return
fi
@@ -258,24 +271,51 @@ build_dotnet_install_commands() {
prompt_and_install_dotnet() {
local label="$1"
local package_kind="$2"
+ local required="${3:-true}"
+ local required_message="${4:-$label is required before installing Quasar.}"
local package_manager
package_manager="$(detect_package_manager)"
if [[ -z "$package_manager" ]]; then
- echo "$label is required before installing Quasar." >&2
+ if [[ "$required" == "true" ]]; then
+ echo "$label is required before installing Quasar." >&2
+ else
+ echo "$label was not installed." >&2
+ fi
echo "No supported package manager found. Install it first: https://dotnet.microsoft.com/download/dotnet/$REQUIRED_DOTNET_MAJOR.0" >&2
- exit 1
+ if [[ "$required" == "true" ]]; then
+ exit 1
+ fi
+ return 1
fi
local packages
packages="$(dotnet_package_for_manager "$package_kind" "$package_manager")"
+ if [[ "$required" != "true" && "${EUID}" -ne 0 ]] && ! have sudo; then
+ echo "$label was not installed." >&2
+ echo "sudo is required to install system packages. Install it manually: https://dotnet.microsoft.com/download/dotnet/$REQUIRED_DOTNET_MAJOR.0" >&2
+ return 1
+ fi
+
if ! build_dotnet_install_commands "$package_manager" "$packages"; then
- echo "$label is required before installing Quasar." >&2
+ if [[ "$required" == "true" ]]; then
+ echo "$label is required before installing Quasar." >&2
+ else
+ echo "$label was not installed." >&2
+ fi
echo "Unsupported package manager '$package_manager'. Install it first: https://dotnet.microsoft.com/download/dotnet/$REQUIRED_DOTNET_MAJOR.0" >&2
- exit 1
+ if [[ "$required" == "true" ]]; then
+ exit 1
+ fi
+ return 1
fi
- echo "$label is required before installing Quasar."
+ if [[ "$required" == "true" ]]; then
+ echo "$required_message"
+ else
+ echo "$label is optional for QuasarHub UI plugin builds."
+ echo "The runtime can run Quasar; the SDK is needed when Quasar compiles source-built UI plugins."
+ fi
echo "Quasar can install it with the detected package manager: $package_manager"
echo
echo "Exact commands to run:"
@@ -285,9 +325,17 @@ prompt_and_install_dotnet() {
done
echo
echo "These commands may add or update system packages. The final command only links dotnet onto PATH if the package manager did not."
+ if [[ "$ASSUME_YES" == "true" ]]; then
+ echo "Running commands without confirmation because --yes was passed."
+ else
if [[ ! -t 0 ]]; then
- echo "Non-interactive terminal; refusing automatic .NET installation." >&2
- exit 1
+ if [[ "$required" == "true" ]]; then
+ echo "Non-interactive terminal; refusing automatic .NET installation." >&2
+ exit 1
+ fi
+
+ echo "Non-interactive terminal; skipping optional $label installation." >&2
+ return 1
fi
local answer
@@ -296,10 +344,16 @@ prompt_and_install_dotnet() {
y|Y|yes|YES|Yes)
;;
*)
- echo "Cancelled. Install $label manually, then rerun install.sh." >&2
- exit 1
+ if [[ "$required" == "true" ]]; then
+ echo "Cancelled. Install $label manually, then rerun install.sh." >&2
+ exit 1
+ fi
+
+ echo "Skipped optional $label installation." >&2
+ return 1
;;
esac
+ fi
for command in "${DOTNET_INSTALL_COMMANDS[@]}"; do
echo "+ $command"
@@ -349,6 +403,54 @@ require_dotnet_installation() {
exit 1
}
+ensure_optional_ui_plugin_sdk() {
+ if [[ "$SKIP_BUILD" == "false" ]]; then
+ return
+ fi
+
+ if dotnet_has_sdk; then
+ echo ".NET $REQUIRED_DOTNET_MAJOR SDK available for QuasarHub UI plugin builds."
+ return
+ fi
+
+ if [[ "$INSTALL_UI_PLUGIN_SDK" == "true" ]]; then
+ prompt_and_install_dotnet ".NET $REQUIRED_DOTNET_MAJOR SDK for QuasarHub UI plugin builds" "sdk"
+ if dotnet_has_sdk; then
+ return
+ fi
+
+ echo ".NET $REQUIRED_DOTNET_MAJOR SDK is still missing after installation attempt." >&2
+ print_installed_dotnet_state
+ exit 1
+ fi
+
+ if [[ ! -t 0 ]]; then
+ echo "Optional .NET $REQUIRED_DOTNET_MAJOR SDK not installed; Quasar can run, but QuasarHub UI plugin install/update is disabled until the SDK is installed." >&2
+ echo "Rerun install.sh with --install-ui-plugin-sdk to install it." >&2
+ return
+ fi
+
+ echo
+ echo "Optional .NET $REQUIRED_DOTNET_MAJOR SDK for QuasarHub UI plugin builds is not installed."
+ echo "The ASP.NET Core runtime can run Quasar; source-built UI plugin install/update uses dotnet build."
+
+ local answer
+ read -r -p "Install the SDK for UI plugin builds now? [y/N] " answer
+ case "$answer" in
+ y|Y|yes|YES|Yes)
+ prompt_and_install_dotnet ".NET $REQUIRED_DOTNET_MAJOR SDK for QuasarHub UI plugin builds" "sdk" false || true
+ if dotnet_has_sdk; then
+ echo ".NET $REQUIRED_DOTNET_MAJOR SDK available for QuasarHub UI plugin builds."
+ else
+ echo "Skipped optional SDK install. QuasarHub UI plugin install/update stays disabled until the SDK is installed." >&2
+ fi
+ ;;
+ *)
+ echo "Skipping optional SDK install. QuasarHub UI plugin install/update stays disabled until the SDK is installed."
+ ;;
+ esac
+}
+
while [[ $# -gt 0 ]]; do
case "$1" in
--install-dir)
@@ -407,6 +509,22 @@ while [[ $# -gt 0 ]]; do
SKIP_BUILD=true
shift
;;
+ --install-ui-plugin-sdk)
+ INSTALL_UI_PLUGIN_SDK=true
+ shift
+ ;;
+ --install-ui-plugin-sdk-only)
+ INSTALL_UI_PLUGIN_SDK=true
+ INSTALL_UI_PLUGIN_SDK_ONLY=true
+ SKIP_BUILD=true
+ ENABLE_SERVICE=false
+ START_SERVICE=false
+ shift
+ ;;
+ --yes|-y)
+ ASSUME_YES=true
+ shift
+ ;;
-h|--help)
usage
exit 0
@@ -432,6 +550,27 @@ if [[ "$(uname -s)" != "Linux" ]]; then
exit 1
fi
+if [[ "$INSTALL_UI_PLUGIN_SDK_ONLY" == "true" ]]; then
+ if dotnet_has_sdk; then
+ echo ".NET $REQUIRED_DOTNET_MAJOR SDK available for QuasarHub UI plugin builds."
+ exit 0
+ fi
+
+ prompt_and_install_dotnet \
+ ".NET $REQUIRED_DOTNET_MAJOR SDK for QuasarHub UI plugin builds" \
+ "sdk" \
+ "true" \
+ ".NET $REQUIRED_DOTNET_MAJOR SDK is required to install or update source-built QuasarHub UI plugins."
+ if dotnet_has_sdk; then
+ echo ".NET $REQUIRED_DOTNET_MAJOR SDK available for QuasarHub UI plugin builds."
+ exit 0
+ fi
+
+ echo ".NET $REQUIRED_DOTNET_MAJOR SDK is still missing after installation attempt." >&2
+ print_installed_dotnet_state
+ exit 1
+fi
+
if [[ "$INSTALL_RENICE_HELPER" == "true" && "$SKIP_BUILD" == "true" && "$ENABLE_SERVICE" == "false" && "$START_SERVICE" == "false" ]]; then
install_renice_helper
exit 0
@@ -497,6 +636,7 @@ case "$DATA_DIR" in
esac
require_dotnet_installation
+ensure_optional_ui_plugin_sdk
write_service_unit() {
if [[ "$INSTALL_MODE" == "system" ]]; then
diff --git a/scripts/readme-install-linux.md b/scripts/readme-install-linux.md
index fa7f7021..436d9c49 100644
--- a/scripts/readme-install-linux.md
+++ b/scripts/readme-install-linux.md
@@ -19,12 +19,19 @@ port is configurable — see [Configuration](Docs/Configuration.md).
### Install as a background service (systemd)
-Install the **.NET 10 runtime** before running `install.sh`.
+Install the **.NET 10 runtime** before running `install.sh`. Packaged Quasar can
+run with the runtime alone, but QuasarHub UI plugin install/update compiles
+source with `dotnet build` and needs the **.NET 10 SDK**. Accept the optional SDK
+prompt or pass `--install-ui-plugin-sdk` if you plan to install source-built UI
+plugins from QuasarHub. If the SDK is still missing later, the UI Plugins page
+can run the install script's SDK-only path from the warning banner.
```bash
mkdir -p ~/.local/share/Quasar
tar -xzf quasar-installer-linux.tar.gz -C ~/.local/share/Quasar --strip-components=1
~/.local/share/Quasar/install.sh --start
+# Or install the optional UI plugin build SDK too:
+# ~/.local/share/Quasar/install.sh --start --install-ui-plugin-sdk
```
This installs Quasar in the extracted folder, keeps Quasar state in the same