v1.0.1
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# Citizen Launcher 1.0.0
|
||||
# Citizen Launcher 1.0.1
|
||||
|
||||
Citizen Launcher is a distro-neutral Star Citizen setup, launch, repair and maintenance application for Linux. Omarchy support is optional; the core is one static Go binary shared by Debian/Ubuntu, Fedora, Arch/Omarchy and other desktop distributions.
|
||||
|
||||
@@ -30,7 +30,7 @@ The launcher owns the fragile user-space gaming stack so users do not have to pi
|
||||
Install the release `.deb`:
|
||||
|
||||
```bash
|
||||
sudo apt install ./citizen-launcher_1.0.0_amd64.deb
|
||||
sudo apt install ./citizen-launcher_1.0.1_amd64.deb
|
||||
```
|
||||
|
||||
The package adds the desktop application and enables the system package-update timer. Future Citizen Launcher `.deb` releases can be installed automatically after release-asset digest and package metadata verification.
|
||||
|
||||
+22
-33
@@ -1,42 +1,31 @@
|
||||
# Citizen Launcher 1.0.0
|
||||
# Citizen Launcher 1.0.1
|
||||
|
||||
First productized distro-neutral release.
|
||||
Reliability hotfix based on the first real-machine 1.0.0 acceptance bundle.
|
||||
|
||||
## Setup and gameplay
|
||||
## Fixed
|
||||
|
||||
- Replaces the old LUG-controlled installation path with one Citizen Launcher core.
|
||||
- Chooses the newest stable LUG Wine release that actually passes a fresh-prefix self-test on the local CPU/glibc environment; incompatible releases are cached and skipped.
|
||||
- Preserves a previous working Wine runner and never activates an untested candidate.
|
||||
- Uses a deterministic SHA-256-pinned Winetricks 20260125 base (`arial`, `tahoma`, `win11` only).
|
||||
- Avoids the unreliable Winetricks PowerShell MSI path. RSI PowerShell compatibility is provided by verified portable PowerShell Core 7.4.19 and the RSI-compatible Wine wrapper 3.0.5.
|
||||
- Manages DXVK itself, including DLL verification and Wine native overrides.
|
||||
- Parses current Electron Builder RSI `latest.yml` layouts and verifies the RSI installer SHA-512 before execution.
|
||||
- Repairs prefix components, RSI compatibility, DXVK, RSI Launcher and desktop integration without deleting game data.
|
||||
- Vulkan hardware detection now evaluates individual `vulkaninfo --summary` GPU blocks instead of treating the presence of any software ICD as a software-only system.
|
||||
- A real AMD/NVIDIA/Intel Vulkan GPU is accepted even when Mesa also exposes `llvmpipe`/lavapipe/softpipe.
|
||||
- Discrete GPUs are preferred for display/status, then integrated GPUs, then non-software virtual GPUs.
|
||||
- QEMU/bochs remains blocked when it is the only usable graphics path, but no longer blocks a machine that also exposes a real working Vulkan GPU.
|
||||
- Vulkan API compatibility is evaluated on the selected real GPU rather than the first arbitrary device in the summary.
|
||||
- PowerShell compatibility self-test no longer requires stdout to be relayed through Wine. PowerShell Core is tested directly and the RSI-compatible wrapper is tested by its propagated exit status.
|
||||
- Game/support status now discovers Prefix, DXVK, PowerShell, RSI Launcher and game files before applying an overall hardware/preflight block. A blocked machine therefore no longer reports existing components as falsely `missing`.
|
||||
|
||||
## Reliability hardening
|
||||
## Regression coverage
|
||||
|
||||
- Fixes the historical literal `\\n` Star Citizen desktop-entry regression and automatically repairs old affected launchers.
|
||||
- Fixes Wine registry path escaping; the file-association tweak is non-fatal.
|
||||
- Fixes executable permissions when self-sync copies over an existing temporary file.
|
||||
- Single GUI instance via file lock; a second launch reuses the existing localhost UI.
|
||||
- One active GUI job at a time plus a cross-process gaming-stack lock.
|
||||
- Detects a running RSI Launcher or Star Citizen and does not launch duplicates or kill an active game.
|
||||
- Exact `/proc` `WINEPREFIX` matching avoids false busy/running detections for similarly named prefixes.
|
||||
- Wine and DXVK release archives use safe extraction with traversal/link checks.
|
||||
- Downloads are staged and atomically activated; logs rotate and maintenance is deferred while the prefix is busy.
|
||||
- AMD RADV + llvmpipe must select AMD and report Vulkan ready.
|
||||
- llvmpipe-only must remain rejected.
|
||||
- a successful PowerShell wrapper with empty stdout must pass.
|
||||
- blocked overall health must still expose real installed component states.
|
||||
- existing 1.0 release-gate coverage remains active: race detector, stack/GUI locks, desktop-entry repair, safe archive extraction, self-update validation, package validation and integration tests.
|
||||
|
||||
## System and UX
|
||||
## Upgrade
|
||||
|
||||
- Responsive standalone GUI with concise errors and expandable technical diagnostics.
|
||||
- Preflight for Vulkan, AVX, RAM+swap, free storage, Linux filesystem, `vm.max_map_count` and open-file limits.
|
||||
- Debian package installs the recommended system limits and uses Polkit for interactive preparation when needed.
|
||||
- User-local 0.9.x binaries/desktop entries that shadow a newer Debian package are migrated safely.
|
||||
- Generic uninstall removes launcher integration but preserves game/config data.
|
||||
Debian/Ubuntu/Mint:
|
||||
|
||||
## Updates and maintenance
|
||||
```bash
|
||||
sudo apt install ./citizen-launcher_1.0.1_amd64.deb
|
||||
```
|
||||
|
||||
- Debian package installs a systemd timer that checks for a new Citizen Launcher release about every six hours.
|
||||
- Automatic package update requires the expected GitHub release asset, a SHA-256 digest, matching `citizen-launcher` package name/version and `amd64` architecture before APT is invoked.
|
||||
- Root updater ignores user-controlled release-repository environment variables and rejects writable/non-root repository configuration.
|
||||
- Running GUIs are not killed during package replacement; they offer a controlled restart into the installed version.
|
||||
- CI/release gate runs unit tests, vet, race detector, static build, integration test and package verification.
|
||||
Existing configuration, Wine prefix, RSI Launcher and Star Citizen data are preserved.
|
||||
|
||||
+25
-31
@@ -1,10 +1,10 @@
|
||||
# Citizen Launcher 1.0.0 – Verification Report
|
||||
# Citizen Launcher 1.0.1 – Verification Report
|
||||
|
||||
Date: 2026-08-31
|
||||
|
||||
## Release gate
|
||||
|
||||
The 1.0.0 source tree passed the complete automated release gate:
|
||||
The 1.0.1 source tree passed the complete automated release gate:
|
||||
|
||||
- shell syntax checks for installer, uninstaller, packaging and integration scripts
|
||||
- `gofmt` cleanliness
|
||||
@@ -15,41 +15,35 @@ The 1.0.0 source tree passed the complete automated release gate:
|
||||
- Omarchy updater integration test against disposable local Git repositories
|
||||
- Debian package build and metadata/payload verification
|
||||
- generic Linux amd64 tarball build and execution check
|
||||
- process-level GUI single-instance test: a second GUI invocation reused the first localhost instance
|
||||
- live GUI `/api/ping` check reporting version 1.0.0
|
||||
- systemd service/timer syntax verification with `systemd-analyze verify`
|
||||
- desktop entry newline regression check (no literal `\\n` in Exec entries)
|
||||
- AppStream XML parse check
|
||||
- GitHub Actions CI/release YAML parse check
|
||||
- systemd service/timer syntax verification
|
||||
- desktop entry regression checks
|
||||
- AppStream and workflow validation inherited from the 1.0 release gate
|
||||
|
||||
## Regression coverage added for 1.0.0
|
||||
## Real-machine regressions reproduced from the support bundle
|
||||
|
||||
The Go test suite explicitly covers:
|
||||
The uploaded Debian 13 support bundle exposed two 1.0.0 false negatives:
|
||||
|
||||
- safe archive extraction and traversal/symlink rejection
|
||||
- exact Wine-prefix process matching
|
||||
- GUI and stack-operation file locks
|
||||
- duplicate primary-action handler regression
|
||||
- Star Citizen/RSI duplicate launch handling
|
||||
- DXVK DLL/override state
|
||||
- deterministic Winetricks checksum and minimal base verbs
|
||||
- legacy user-local binary migration
|
||||
- broken desktop entry repair from the 0.9.2 literal-newline bug
|
||||
- copied executable permission preservation
|
||||
- Debian update package name/version/architecture validation
|
||||
- release asset selection/version comparison
|
||||
- Electron-builder RSI `latest.yml` variants
|
||||
- non-MSI portable PowerShell compatibility path
|
||||
- Wine registry path escaping
|
||||
- support-bundle sanitizer behavior
|
||||
- required SHA-256 release digest fail-closed behavior
|
||||
1. `vulkaninfo` reported both `AMD Radeon Graphics (RADV PHOENIX2)` and Mesa `llvmpipe`. 1.0.0 rejected the whole system merely because the software ICD was present. 1.0.1 parses devices independently and selects the real AMD GPU.
|
||||
2. The RSI PowerShell wrapper returned a successful process exit but no captured stdout. 1.0.0 incorrectly required a marker string in stdout. 1.0.1 validates PowerShell Core and then treats the wrapper's propagated exit status as authoritative.
|
||||
|
||||
Automated regression tests cover both conditions.
|
||||
|
||||
## Additional process-level verification
|
||||
|
||||
A real two-process GUI test was run against the built binary:
|
||||
|
||||
- first `gui --no-open` created a localhost endpoint
|
||||
- second `gui --no-open` returned the exact same endpoint instead of creating another backend
|
||||
- `/api/ping` on that endpoint reported version `1.0.1`
|
||||
|
||||
A synthetic `vulkaninfo` run matching the support bundle's AMD + llvmpipe layout selected `AMD Radeon Graphics (RADV PHOENIX2)` and reported Vulkan `ready`. The sandbox itself has insufficient RAM for Star Citizen, so its overall hardware state remained blocked for RAM as expected; the GPU path was no longer the blocker.
|
||||
|
||||
## Final binary / package hashes
|
||||
|
||||
- `backend/bin/citizen-launcher`: `5e55630dc47b89a11185384f09d79fb3813fce51b4d0e30a9660e6526c93a46d`
|
||||
- `dist/citizen-launcher_1.0.0_amd64.deb`: `976a4cb3efc193f9f05e880ab0456d80e49cb7b5db0a61621d4dc4a788e7c8a1`
|
||||
- `dist/citizen-launcher-1.0.0-linux-amd64.tar.gz`: `551bca5656382f79fd6e388d14f58555d0db66418a0388ab8a8ba2ac1a058e90`
|
||||
- `backend/bin/citizen-launcher`: `7cd0d85046b2c9ac295c6ef5b86f93a3452bfb3cd94cff048e7511db581f3d07`
|
||||
- `dist/citizen-launcher_1.0.1_amd64.deb`: `aba0944a0d2334f14f425e12a55ddd28d011449387e5be1ef3e0731e9d9a5119`
|
||||
- `dist/citizen-launcher-1.0.1-linux-amd64.tar.gz`: `59edf81938c305b1d86d917d38222470b4f5e42c9c3dc350bf1ac430d8748bfc`
|
||||
|
||||
## Scope boundary
|
||||
|
||||
The release gate validates the launcher, package, updater, GUI, locks, migration, install/repair logic and local safety properties in the build environment. It cannot perform an RSI account login, download the full Star Citizen game, or execute a real game session with an external GPU from this sandbox. Those remain real-machine acceptance tests rather than simulated release-gate claims.
|
||||
The release gate validates the launcher, package, updater, GUI, locks, migration, install/repair logic and local safety properties. It cannot perform an RSI account login or a complete live Star Citizen game session from this sandbox. Those remain real-machine acceptance tests.
|
||||
|
||||
Binary file not shown.
@@ -16,7 +16,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
appVersion = "1.0.0"
|
||||
appVersion = "1.0.1"
|
||||
releaseRepo = "sendnwv/omarchy-sc"
|
||||
)
|
||||
|
||||
|
||||
@@ -147,20 +147,10 @@ func (a *App) gameStatus() GameStatus {
|
||||
Autopilot: uc.AutoMaintain,
|
||||
System: sys,
|
||||
}
|
||||
if err := validatePrefixPath(gc.Prefix); err != nil {
|
||||
st.Health = "hardware-blocked"
|
||||
st.HardwareReason = err.Error()
|
||||
return st
|
||||
}
|
||||
if hs.State == "blocked" {
|
||||
st.Health = "hardware-blocked"
|
||||
return st
|
||||
}
|
||||
if sys.State == "blocked" {
|
||||
st.Health = "hardware-blocked"
|
||||
st.HardwareReason = sys.Reason
|
||||
return st
|
||||
}
|
||||
// Always inspect the existing stack before deciding the overall health.
|
||||
// Support bundles must remain truthful even when hardware/preflight is
|
||||
// blocked; otherwise a perfectly valid prefix/launcher appears as missing.
|
||||
prefixPathErr := validatePrefixPath(gc.Prefix)
|
||||
if prefixInitialized(gc.Prefix) {
|
||||
st.PrefixState = "ready"
|
||||
} else if fi, err := os.Stat(gc.Prefix); err == nil && fi.IsDir() {
|
||||
@@ -179,6 +169,23 @@ func (a *App) gameStatus() GameStatus {
|
||||
st.GameState = "partial"
|
||||
}
|
||||
|
||||
// Health precedence is evaluated only after component discovery so diagnosis
|
||||
// never loses useful state.
|
||||
if prefixPathErr != nil {
|
||||
st.Health = "hardware-blocked"
|
||||
st.HardwareReason = prefixPathErr.Error()
|
||||
return st
|
||||
}
|
||||
if hs.State == "blocked" {
|
||||
st.Health = "hardware-blocked"
|
||||
return st
|
||||
}
|
||||
if sys.State == "blocked" {
|
||||
st.Health = "hardware-blocked"
|
||||
st.HardwareReason = sys.Reason
|
||||
return st
|
||||
}
|
||||
|
||||
// Around 150 GiB of free space is the current LUG quick-start recommendation
|
||||
// for a fresh install. Do not block an already-installed game because free
|
||||
// space naturally drops after installation.
|
||||
@@ -222,6 +229,90 @@ type hardwareInfo struct {
|
||||
DiskFreeGiB int
|
||||
}
|
||||
|
||||
type vulkanDevice struct {
|
||||
Name string
|
||||
Type string
|
||||
APIMajor int
|
||||
APIMinor int
|
||||
}
|
||||
|
||||
func parseVulkanDevices(summary string) []vulkanDevice {
|
||||
var devices []vulkanDevice
|
||||
var current *vulkanDevice
|
||||
flush := func() {
|
||||
if current != nil && (current.Name != "" || current.Type != "" || current.APIMajor != 0) {
|
||||
devices = append(devices, *current)
|
||||
}
|
||||
}
|
||||
apiRE := regexp.MustCompile(`^([0-9]+)\.([0-9]+)`)
|
||||
gpuRE := regexp.MustCompile(`^GPU[0-9]+:$`)
|
||||
for _, raw := range strings.Split(summary, "\n") {
|
||||
line := strings.TrimSpace(raw)
|
||||
if gpuRE.MatchString(line) {
|
||||
flush()
|
||||
current = &vulkanDevice{}
|
||||
continue
|
||||
}
|
||||
if current == nil {
|
||||
continue
|
||||
}
|
||||
key, val, ok := strings.Cut(line, "=")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
key, val = strings.TrimSpace(key), strings.TrimSpace(val)
|
||||
switch key {
|
||||
case "deviceName":
|
||||
current.Name = val
|
||||
case "deviceType":
|
||||
current.Type = val
|
||||
case "apiVersion":
|
||||
if m := apiRE.FindStringSubmatch(val); len(m) == 3 {
|
||||
current.APIMajor, _ = strconv.Atoi(m[1])
|
||||
current.APIMinor, _ = strconv.Atoi(m[2])
|
||||
}
|
||||
}
|
||||
}
|
||||
flush()
|
||||
return devices
|
||||
}
|
||||
|
||||
func softwareVulkanDevice(d vulkanDevice) bool {
|
||||
low := strings.ToLower(d.Name + " " + d.Type)
|
||||
return strings.Contains(low, "physical_device_type_cpu") ||
|
||||
strings.Contains(low, "llvmpipe") || strings.Contains(low, "lavapipe") || strings.Contains(low, "softpipe")
|
||||
}
|
||||
|
||||
func vulkanDeviceRank(d vulkanDevice) int {
|
||||
if softwareVulkanDevice(d) {
|
||||
return -1
|
||||
}
|
||||
switch strings.ToUpper(d.Type) {
|
||||
case "PHYSICAL_DEVICE_TYPE_DISCRETE_GPU":
|
||||
return 30
|
||||
case "PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU":
|
||||
return 20
|
||||
case "PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU":
|
||||
return 10
|
||||
default:
|
||||
if d.Name != "" {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func selectVulkanHardwareDevice(summary string) (vulkanDevice, bool) {
|
||||
best, rank := vulkanDevice{}, -1
|
||||
for _, d := range parseVulkanDevices(summary) {
|
||||
r := vulkanDeviceRank(d)
|
||||
if r > rank {
|
||||
best, rank = d, r
|
||||
}
|
||||
}
|
||||
return best, rank >= 0
|
||||
}
|
||||
|
||||
func (a *App) hardwareStatus(prefix string) hardwareInfo {
|
||||
h := hardwareInfo{State: "ready", Vulkan: "unknown"}
|
||||
if runtime.GOARCH != "amd64" {
|
||||
@@ -267,8 +358,8 @@ func (a *App) hardwareStatus(prefix string) hardwareInfo {
|
||||
pci, _ := exec.Command("lspci", "-nnk").CombinedOutput()
|
||||
p := string(pci)
|
||||
if strings.TrimSpace(p) == "" {
|
||||
// lspci/pciutils is optional on minimal Debian installations. Detect the
|
||||
// QEMU VGA device through sysfs as a dependency-free fallback.
|
||||
// lspci/pciutils is optional on minimal installations. Detect QEMU VGA
|
||||
// through sysfs as a dependency-free fallback.
|
||||
vendors, _ := filepath.Glob("/sys/class/drm/card*/device/vendor")
|
||||
for _, vp := range vendors {
|
||||
v, _ := os.ReadFile(vp)
|
||||
@@ -287,45 +378,48 @@ func (a *App) hardwareStatus(prefix string) hardwareInfo {
|
||||
}
|
||||
}
|
||||
}
|
||||
lowp := strings.ToLower(p)
|
||||
if strings.Contains(lowp, "1234:1111") || strings.Contains(lowp, "kernel driver in use: bochs") {
|
||||
h.State = "blocked"
|
||||
h.Vulkan = "blocked"
|
||||
h.Reason = "QEMU Standard VGA/bochs-drm erkannt. Star Citizen benötigt eine echte Vulkan-GPU oder GPU-Passthrough."
|
||||
return h
|
||||
}
|
||||
qemuFallback := strings.Contains(strings.ToLower(p), "1234:1111") || strings.Contains(strings.ToLower(p), "kernel driver in use: bochs")
|
||||
|
||||
if path, err := exec.LookPath("vulkaninfo"); err == nil {
|
||||
cmd := exec.Command(path, "--summary")
|
||||
out, err := cmd.CombinedOutput()
|
||||
text := string(out)
|
||||
low := strings.ToLower(text)
|
||||
if err != nil || strings.Contains(text, "VK_ERROR_INCOMPATIBLE_DRIVER") {
|
||||
h.State = "blocked"
|
||||
h.Vulkan = "blocked"
|
||||
h.Reason = "Vulkan ist nicht funktionsfähig."
|
||||
return h
|
||||
}
|
||||
if strings.Contains(low, "device_type_cpu") || strings.Contains(low, "llvmpipe") || strings.Contains(low, "lavapipe") || strings.Contains(low, "softpipe") {
|
||||
dev, hasHardware := selectVulkanHardwareDevice(text)
|
||||
if !hasHardware {
|
||||
h.State = "blocked"
|
||||
h.Vulkan = "blocked"
|
||||
h.Reason = "Es wurde nur ein Software-Vulkan-Gerät erkannt. Star Citizen benötigt eine echte Vulkan-GPU."
|
||||
if qemuFallback {
|
||||
h.Reason = "QEMU Standard VGA/bochs-drm erkannt und keine echte Vulkan-GPU verfügbar. Für Star Citizen ist GPU-Passthrough erforderlich."
|
||||
} else {
|
||||
h.Reason = "Es wurde nur ein Software-Vulkan-Gerät erkannt. Star Citizen benötigt eine echte Vulkan-GPU."
|
||||
}
|
||||
return h
|
||||
}
|
||||
if m := regexp.MustCompile(`(?m)deviceName\s*=\s*(.+)$`).FindStringSubmatch(text); len(m) == 2 {
|
||||
h.GPU = strings.TrimSpace(m[1])
|
||||
if dev.Name != "" {
|
||||
h.GPU = dev.Name
|
||||
}
|
||||
if m := regexp.MustCompile(`(?m)apiVersion\s*=\s*([0-9]+)\.([0-9]+)`).FindStringSubmatch(text); len(m) == 3 {
|
||||
major, _ := strconv.Atoi(m[1])
|
||||
minor, _ := strconv.Atoi(m[2])
|
||||
if major < 1 || (major == 1 && minor < 3) {
|
||||
h.State = "blocked"
|
||||
h.Vulkan = "blocked"
|
||||
h.Reason = fmt.Sprintf("Vulkan %d.%d erkannt; der aktuelle DXVK-Stack benötigt Vulkan 1.3 oder neuer.", major, minor)
|
||||
return h
|
||||
}
|
||||
if dev.APIMajor < 1 || (dev.APIMajor == 1 && dev.APIMinor < 3) {
|
||||
h.State = "blocked"
|
||||
h.Vulkan = "blocked"
|
||||
h.Reason = fmt.Sprintf("Vulkan %d.%d erkannt; der aktuelle DXVK-Stack benötigt Vulkan 1.3 oder neuer.", dev.APIMajor, dev.APIMinor)
|
||||
return h
|
||||
}
|
||||
h.Vulkan = "ready"
|
||||
} else {
|
||||
// Without vulkaninfo we can only make a provisional assessment. QEMU's
|
||||
// bochs framebuffer is still a hard block because it is not a Vulkan GPU.
|
||||
if qemuFallback {
|
||||
h.State = "blocked"
|
||||
h.Vulkan = "blocked"
|
||||
h.Reason = "QEMU Standard VGA/bochs-drm erkannt. Star Citizen benötigt eine echte Vulkan-GPU oder GPU-Passthrough."
|
||||
return h
|
||||
}
|
||||
matches, _ := filepath.Glob("/usr/share/vulkan/icd.d/*.json")
|
||||
if len(matches) > 0 {
|
||||
h.Vulkan = "probable"
|
||||
@@ -886,17 +980,27 @@ func (a *App) writePowerShellMarker(prefix, version string) error {
|
||||
}
|
||||
|
||||
func (a *App) probePowerShell(gc GameConfig, env []string) error {
|
||||
_, _, wrapper64, _ := a.powerShellPaths(gc.Prefix)
|
||||
core, _, wrapper64, _ := a.powerShellPaths(gc.Prefix)
|
||||
runner, err := a.currentRunner()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
wine := filepath.Join(runner, "bin", "wine")
|
||||
probe := exec.Command(wine, wrapper64, "-NoLogo", "-NoProfile", "-NonInteractive", "-Command", "Write-Output CITIZEN_POWERSHELL_OK")
|
||||
probe.Env = env
|
||||
out, err := probe.CombinedOutput()
|
||||
if err != nil || !strings.Contains(string(out), "CITIZEN_POWERSHELL_OK") {
|
||||
return fmt.Errorf("PowerShell Selbsttest: %s", formatCommandFailure(err, out))
|
||||
|
||||
// Validate PowerShell Core directly first. Then validate the compatibility
|
||||
// wrapper using the invocation shape documented by the wrapper project.
|
||||
// Wine does not reliably forward stdout from a Windows child spawned by
|
||||
// another Windows executable, while wrapper v3+ deliberately propagates the
|
||||
// child's exit status. A successful exit is therefore authoritative.
|
||||
coreProbe := exec.Command(wine, core, "-NoLogo", "-NoProfile", "-NonInteractive", "-Command", "exit 0")
|
||||
coreProbe.Env = env
|
||||
if out, err := coreProbe.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("PowerShell Core Selbsttest: %s", formatCommandFailure(err, out))
|
||||
}
|
||||
wrapperProbe := exec.Command(wine, wrapper64, "-NoLogo", "-NonInteractive", "exit 0")
|
||||
wrapperProbe.Env = env
|
||||
if out, err := wrapperProbe.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("PowerShell Wrapper Selbsttest: %s", formatCommandFailure(err, out))
|
||||
}
|
||||
wait := exec.Command(filepath.Join(runner, "bin", "wineserver"), "-w")
|
||||
wait.Env = env
|
||||
|
||||
@@ -179,3 +179,116 @@ func TestRequiredReleaseDigestFailsClosed(t *testing.T) {
|
||||
t.Fatalf("known SHA-256 rejected: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVulkanHardwareSelectionIgnoresLLVMPipeWhenRealGPUExists(t *testing.T) {
|
||||
summary := `Devices:
|
||||
========
|
||||
GPU0:
|
||||
apiVersion = 1.4.305
|
||||
deviceType = PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU
|
||||
deviceName = AMD Radeon Graphics (RADV PHOENIX2)
|
||||
GPU1:
|
||||
apiVersion = 1.4.305
|
||||
deviceType = PHYSICAL_DEVICE_TYPE_CPU
|
||||
deviceName = llvmpipe (LLVM 19.1.7, 256 bits)
|
||||
`
|
||||
dev, ok := selectVulkanHardwareDevice(summary)
|
||||
if !ok {
|
||||
t.Fatal("real Vulkan GPU was rejected because a software ICD was also present")
|
||||
}
|
||||
if dev.Name != "AMD Radeon Graphics (RADV PHOENIX2)" {
|
||||
t.Fatalf("selected GPU=%q", dev.Name)
|
||||
}
|
||||
if dev.APIMajor != 1 || dev.APIMinor != 4 {
|
||||
t.Fatalf("selected Vulkan version=%d.%d", dev.APIMajor, dev.APIMinor)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVulkanHardwareSelectionRejectsSoftwareOnly(t *testing.T) {
|
||||
summary := `Devices:
|
||||
========
|
||||
GPU0:
|
||||
apiVersion = 1.4.305
|
||||
deviceType = PHYSICAL_DEVICE_TYPE_CPU
|
||||
deviceName = llvmpipe (LLVM 19.1.7, 256 bits)
|
||||
`
|
||||
if dev, ok := selectVulkanHardwareDevice(summary); ok {
|
||||
t.Fatalf("software-only Vulkan device accepted: %#v", dev)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPowerShellProbeAcceptsSuccessfulWrapperWithoutStdout(t *testing.T) {
|
||||
a := testApp(t)
|
||||
prefix := filepath.Join(a.home, "Games", "star-citizen")
|
||||
gc := GameConfig{Prefix: prefix}
|
||||
core, _, wrapper64, _ := a.powerShellPaths(prefix)
|
||||
for _, p := range []string{core, wrapper64} {
|
||||
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(p, make([]byte, 2048), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
runner := filepath.Join(a.vendorDir, "wine", "fixture")
|
||||
bin := filepath.Join(runner, "bin")
|
||||
if err := os.MkdirAll(bin, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The fixture intentionally emits no stdout: this reproduces the real Wine
|
||||
// behavior from the 1.0.0 support bundle.
|
||||
for _, name := range []string{"wine", "wineserver"} {
|
||||
p := filepath.Join(bin, name)
|
||||
if err := os.WriteFile(p, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(a.vendorDir, "wine"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Symlink(runner, filepath.Join(a.vendorDir, "wine", "current")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := a.probePowerShell(gc, os.Environ()); err != nil {
|
||||
t.Fatalf("successful wrapper without stdout was rejected: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGameStatusStillReportsInstalledComponentsWhenHealthIsBlocked(t *testing.T) {
|
||||
a := testApp(t)
|
||||
prefix := filepath.Join(a.home, "Games", "star citizen") // intentionally invalid path: forces blocked health
|
||||
gc := GameConfig{
|
||||
Prefix: prefix,
|
||||
LauncherEXE: filepath.Join(prefix, "drive_c", "Program Files", "Roberts Space Industries", "RSI Launcher", "RSI Launcher.exe"),
|
||||
GameDir: filepath.Join(prefix, "drive_c", "Program Files", "Roberts Space Industries", "StarCitizen"),
|
||||
}
|
||||
for _, p := range []string{
|
||||
filepath.Join(prefix, "drive_c"),
|
||||
filepath.Dir(gc.LauncherEXE),
|
||||
filepath.Join(gc.GameDir, "LIVE", "Bin64"),
|
||||
} {
|
||||
if err := os.MkdirAll(p, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for _, p := range []string{
|
||||
filepath.Join(prefix, "system.reg"),
|
||||
filepath.Join(prefix, "user.reg"),
|
||||
gc.LauncherEXE,
|
||||
filepath.Join(gc.GameDir, "LIVE", "Bin64", "StarCitizen.exe"),
|
||||
} {
|
||||
if err := os.WriteFile(p, []byte("fixture"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := a.saveGameConfig(gc); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
st := a.gameStatus()
|
||||
if st.Health != "hardware-blocked" {
|
||||
t.Fatalf("health=%q want hardware-blocked", st.Health)
|
||||
}
|
||||
if st.PrefixState != "ready" || st.LauncherState != "ready" || st.GameState != "ready" {
|
||||
t.Fatalf("blocked health hid real component state: prefix=%s launcher=%s game=%s", st.PrefixState, st.LauncherState, st.GameState)
|
||||
}
|
||||
}
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -12,7 +12,7 @@ Panel {
|
||||
property var anchorItem: null
|
||||
property var hostWidget: null
|
||||
|
||||
property string pluginVersion: "1.0.0"
|
||||
property string pluginVersion: "1.0.1"
|
||||
property string health: "checking"
|
||||
property string depsState: "checking"
|
||||
property string depsMissing: ""
|
||||
@@ -106,7 +106,7 @@ Panel {
|
||||
values[lines[i].slice(0, p)] = lines[i].slice(p + 1)
|
||||
}
|
||||
|
||||
pluginVersion = values.plugin_version || "1.0.0"
|
||||
pluginVersion = values.plugin_version || "1.0.1"
|
||||
health = values.health || "setup"
|
||||
depsState = values.deps || "missing"
|
||||
depsMissing = values.deps_missing || ""
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
set -u
|
||||
|
||||
PLUGIN_ID="local.omarchy-citizen"
|
||||
PLUGIN_VERSION="1.0.0"
|
||||
PLUGIN_VERSION="1.0.1"
|
||||
|
||||
XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
|
||||
XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"schemaVersion": 1,
|
||||
"id": "local.omarchy-citizen",
|
||||
"name": "Citizen Launcher · Omarchy",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"author": "Community prototype",
|
||||
"license": "MIT",
|
||||
"description": "Optional Omarchy bar integration for the distro-neutral Citizen Launcher.",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: citizen-launcher
|
||||
Version: 1.0.0
|
||||
Version: 1.0.1
|
||||
Release: 1%{?dist}
|
||||
Summary: Star Citizen launcher and self-maintaining Wine stack for Linux
|
||||
License: MIT
|
||||
|
||||
@@ -13,6 +13,6 @@
|
||||
<url type="homepage">https://github.com/sendnwv/omarchy-sc</url>
|
||||
<categories><category>Game</category></categories>
|
||||
<provides><binary>citizen-launcher</binary></provides>
|
||||
<releases><release version="1.0.0" date="2026-08-31"/></releases>
|
||||
<releases><release version="1.0.1" date="2026-08-31"/><release version="1.0.0" date="2026-08-31"/></releases>
|
||||
<content_rating type="oars-1.1"/>
|
||||
</component>
|
||||
|
||||
Reference in New Issue
Block a user