v0.9.2
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# Citizen Launcher 0.9.1
|
||||
# Citizen Launcher 0.9.2
|
||||
|
||||
Citizen Launcher is the distro-neutral successor to Omarchy Citizen.
|
||||
|
||||
@@ -81,10 +81,27 @@ For Omarchy plus bar integration:
|
||||
Citizen Launcher manages its Wine/DXVK/RSI stack entirely as the current user. It does not create passwordless sudo/pacman/apt rules. GPU/kernel/base-system updates remain owned by the distribution.
|
||||
|
||||
|
||||
## 0.9.1 fixes
|
||||
## 0.9.2 fixes
|
||||
|
||||
- PowerShell is no longer a mandatory prefix component.
|
||||
- RSI `latest.yml` parser accepts Electron Builder `path:` and nested `files: - url:` formats.
|
||||
- Repair also refreshes DXVK.
|
||||
- Responsive dashboard prevents long GPU/distribution names from overflowing.
|
||||
- GUI shows concise user-facing errors with expandable technical details.
|
||||
|
||||
## Automatic Citizen Launcher updates
|
||||
|
||||
Starting with 0.9.2 the launcher itself is part of Autopilot.
|
||||
|
||||
### Debian / Ubuntu / Mint
|
||||
|
||||
Installing the `.deb` once enables `citizen-launcher-self-update.timer`. Future
|
||||
Citizen Launcher releases are checked automatically every six hours. A new `.deb`
|
||||
is only installed after its GitHub SHA-256 asset digest and Debian package metadata
|
||||
have been verified.
|
||||
|
||||
The currently running GUI is never killed during package replacement. If an update
|
||||
landed while the GUI was open, the app shows **Neue Version installiert** and offers
|
||||
a controlled restart into the new binary.
|
||||
|
||||
See `packaging/SELF_UPDATE.md` for details.
|
||||
|
||||
+17
-1
@@ -1,4 +1,4 @@
|
||||
# Citizen Launcher 0.9.1
|
||||
# Citizen Launcher 0.9.2
|
||||
|
||||
Maintenance/UX hotfix for the distro-neutral preview.
|
||||
|
||||
@@ -7,3 +7,19 @@ Maintenance/UX hotfix for the distro-neutral preview.
|
||||
- DXVK is included in automatic repair.
|
||||
- Responsive GUI with safer grid sizing and readable diagnostics.
|
||||
- Added regression tests for RSI metadata formats and the minimal prefix contract.
|
||||
|
||||
## 0.9.2
|
||||
|
||||
- fixes `Wine Registry: reg: Angegebener Schlüssel nicht zugreifbar oder erstellbar`
|
||||
- registry path now uses valid single separators
|
||||
- file-association tweak is non-fatal because it is not required by Star Citizen
|
||||
- adds automatic application updates
|
||||
- Debian package installs a root systemd updater timer
|
||||
- updater checks GitHub Releases every six hours
|
||||
- downloads only the expected `citizen-launcher_<version>_amd64.deb`
|
||||
- requires and verifies GitHub's SHA-256 asset digest
|
||||
- verifies package name/version/architecture before APT installation
|
||||
- generic user installs self-update from the verified tarball
|
||||
- GUI shows whether automatic package updates are active
|
||||
- a running old GUI detects when a newer package was installed and offers a one-click restart
|
||||
- adds a GitHub Actions release workflow and central `VERSION` file
|
||||
|
||||
Binary file not shown.
+4
-1
@@ -1,9 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ROOT="$(cd -- "$(dirname -- "$0")/.." && pwd)"
|
||||
cd "$(dirname "$0")"
|
||||
VERSION="$(tr -d '[:space:]' < "$ROOT/VERSION")"
|
||||
RELEASE_REPO="${CITIZEN_LAUNCHER_RELEASE_REPO:-sendnwv/omarchy-sc}"
|
||||
mkdir -p bin
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
|
||||
-trimpath -ldflags='-s -w' \
|
||||
-trimpath -ldflags="-s -w -X main.appVersion=$VERSION -X main.releaseRepo=$RELEASE_REPO" \
|
||||
-o bin/citizen-launcher \
|
||||
./cmd/citizen-launcher
|
||||
sha256sum bin/citizen-launcher
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -21,10 +22,13 @@ import (
|
||||
var guiFiles embed.FS
|
||||
|
||||
type GUIStatus struct {
|
||||
Version string `json:"version"`
|
||||
Platform PlatformStatus `json:"platform"`
|
||||
Game GameStatus `json:"game"`
|
||||
Launcher Status `json:"launcher"`
|
||||
Version string `json:"version"`
|
||||
InstalledVersion string `json:"installed_version,omitempty"`
|
||||
RestartRequired bool `json:"restart_required"`
|
||||
PackageAutoUpdate bool `json:"package_auto_update"`
|
||||
Platform PlatformStatus `json:"platform"`
|
||||
Game GameStatus `json:"game"`
|
||||
Launcher Status `json:"launcher"`
|
||||
}
|
||||
|
||||
type GUIJob struct {
|
||||
@@ -100,7 +104,11 @@ func (a *App) runGUI(args []string) error {
|
||||
}
|
||||
mux.HandleFunc(base+"/api/status", func(w http.ResponseWriter, r *http.Request) {
|
||||
st, _ := a.status(false)
|
||||
jsonOut(w, GUIStatus{Version: appVersion, Platform: detectPlatform(), Game: a.gameStatus(), Launcher: st})
|
||||
su, _ := a.selfUpdateStatus(false)
|
||||
jsonOut(w, GUIStatus{
|
||||
Version: appVersion, InstalledVersion: su.Installed, RestartRequired: su.RestartNeeded,
|
||||
PackageAutoUpdate: packageAutoUpdateActive(), Platform: detectPlatform(), Game: a.gameStatus(), Launcher: st,
|
||||
})
|
||||
})
|
||||
mux.HandleFunc(base+"/api/job/", func(w http.ResponseWriter, r *http.Request) {
|
||||
id := strings.TrimPrefix(r.URL.Path, base+"/api/job/")
|
||||
@@ -119,6 +127,25 @@ func (a *App) runGUI(args []string) error {
|
||||
return
|
||||
}
|
||||
action := strings.TrimPrefix(r.URL.Path, base+"/api/action/")
|
||||
if action == "restart" {
|
||||
target := a.selfPath
|
||||
if installedPackageVersion() != "" {
|
||||
if _, err := os.Stat("/usr/bin/citizen-launcher"); err == nil {
|
||||
target = "/usr/bin/citizen-launcher"
|
||||
}
|
||||
}
|
||||
if target == "" {
|
||||
http.Error(w, "launcher executable not found", 500)
|
||||
return
|
||||
}
|
||||
if err := exec.Command(target, "gui").Start(); err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
jsonOut(w, map[string]string{"state": "restarting"})
|
||||
go func() { time.Sleep(450 * time.Millisecond); os.Exit(0) }()
|
||||
return
|
||||
}
|
||||
if action == "launch" {
|
||||
if err := a.gameLaunch(); err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
|
||||
@@ -15,10 +15,14 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
appVersion = "0.9.2"
|
||||
releaseRepo = "sendnwv/omarchy-sc"
|
||||
)
|
||||
|
||||
const (
|
||||
appVersion = "0.9.1"
|
||||
pluginID = "local.omarchy-citizen"
|
||||
appID = "io.github.citizenlauncher.CitizenLauncher"
|
||||
pluginID = "local.omarchy-citizen"
|
||||
appID = "io.github.citizenlauncher.CitizenLauncher"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
@@ -108,6 +112,35 @@ func main() {
|
||||
app.logf("manual update failed: %v", err)
|
||||
fatal(err)
|
||||
}
|
||||
case "self-update":
|
||||
if len(args) < 2 {
|
||||
args = append(args, "check")
|
||||
}
|
||||
switch args[1] {
|
||||
case "check":
|
||||
st, err := app.selfUpdateStatus(true)
|
||||
if hasArg(args[2:], "--json") {
|
||||
printJSON(st)
|
||||
} else {
|
||||
printSelfUpdateKV(st)
|
||||
}
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
case "status":
|
||||
st, _ := app.selfUpdateStatus(false)
|
||||
if hasArg(args[2:], "--json") {
|
||||
printJSON(st)
|
||||
} else {
|
||||
printSelfUpdateKV(st)
|
||||
}
|
||||
case "apply":
|
||||
if err := app.applySelfUpdate(hasArg(args[2:], "--system"), hasArg(args[2:], "--quiet")); err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
default:
|
||||
fatal(errors.New("usage: self-update check|status|apply [--system] [--quiet]"))
|
||||
}
|
||||
case "tick":
|
||||
if err := app.tick(); err != nil {
|
||||
app.logf("tick failed: %v", err)
|
||||
@@ -364,6 +397,15 @@ func (a *App) tick() error {
|
||||
if err := a.maintainGamingStack(); err != nil {
|
||||
problems = append(problems, "gaming stack: "+err.Error())
|
||||
}
|
||||
// Generic ~/.local installations can update atomically in the user account.
|
||||
// Debian packages are updated by the root system timer installed by the .deb.
|
||||
if su, err := a.selfUpdateStatus(true); err != nil {
|
||||
a.logf("launcher release check warning: %v", err)
|
||||
} else if su.State == "available" && su.Mode == "user" {
|
||||
if err := a.applySelfUpdate(false, true); err != nil {
|
||||
problems = append(problems, "launcher update: "+err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.AutoApply {
|
||||
|
||||
@@ -0,0 +1,401 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SelfUpdateStatus struct {
|
||||
Current string `json:"current"`
|
||||
Installed string `json:"installed,omitempty"`
|
||||
Latest string `json:"latest,omitempty"`
|
||||
State string `json:"state"`
|
||||
Mode string `json:"mode"`
|
||||
Repository string `json:"repository"`
|
||||
Asset string `json:"asset,omitempty"`
|
||||
Digest string `json:"digest,omitempty"`
|
||||
RestartNeeded bool `json:"restart_needed"`
|
||||
CheckedAt string `json:"checked_at,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func packageAutoUpdateActive() bool {
|
||||
if !commandExists("systemctl") {
|
||||
return false
|
||||
}
|
||||
return exec.Command("systemctl", "is-enabled", "citizen-launcher-self-update.timer").Run() == nil
|
||||
}
|
||||
|
||||
func effectiveReleaseRepo() string {
|
||||
if v := strings.TrimSpace(os.Getenv("CITIZEN_LAUNCHER_RELEASE_REPO")); v != "" {
|
||||
return v
|
||||
}
|
||||
if data, err := os.ReadFile("/etc/citizen-launcher/release-repo"); err == nil {
|
||||
if v := strings.TrimSpace(string(data)); v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return releaseRepo
|
||||
}
|
||||
|
||||
func (a *App) selfUpdateStatus(fetch bool) (SelfUpdateStatus, error) {
|
||||
installed := installedPackageVersion()
|
||||
st := SelfUpdateStatus{
|
||||
Current: appVersion,
|
||||
Installed: installed,
|
||||
State: "not-checked",
|
||||
Mode: a.installMode(),
|
||||
Repository: effectiveReleaseRepo(),
|
||||
}
|
||||
if installed != "" && compareVersions(installed, appVersion) > 0 {
|
||||
st.RestartNeeded = true
|
||||
}
|
||||
if !fetch {
|
||||
if st.RestartNeeded {
|
||||
st.State = "restart-required"
|
||||
}
|
||||
return st, nil
|
||||
}
|
||||
|
||||
release, err := githubLatest(effectiveReleaseRepo())
|
||||
st.CheckedAt = time.Now().Format(time.RFC3339)
|
||||
if err != nil {
|
||||
st.State = "check-failed"
|
||||
st.Error = err.Error()
|
||||
return st, err
|
||||
}
|
||||
latest := strings.TrimPrefix(strings.TrimSpace(release.TagName), "v")
|
||||
if latest == "" {
|
||||
err := errors.New("latest release has no usable version tag")
|
||||
st.State, st.Error = "check-failed", err.Error()
|
||||
return st, err
|
||||
}
|
||||
st.Latest = latest
|
||||
|
||||
asset, err := a.releaseAssetForMode(release, latest, st.Mode)
|
||||
if err != nil {
|
||||
st.State = "asset-missing"
|
||||
st.Error = err.Error()
|
||||
return st, err
|
||||
}
|
||||
st.Asset = asset.Name
|
||||
st.Digest = asset.Digest
|
||||
|
||||
current := appVersion
|
||||
if installed != "" && compareVersions(installed, current) > 0 {
|
||||
current = installed
|
||||
}
|
||||
cmp := compareVersions(latest, current)
|
||||
switch {
|
||||
case cmp > 0:
|
||||
st.State = "available"
|
||||
case st.RestartNeeded:
|
||||
st.State = "restart-required"
|
||||
default:
|
||||
st.State = "current"
|
||||
}
|
||||
return st, nil
|
||||
}
|
||||
|
||||
func (a *App) installMode() string {
|
||||
if installedPackageVersion() != "" && commandExists("dpkg-deb") {
|
||||
return "deb"
|
||||
}
|
||||
return "user"
|
||||
}
|
||||
|
||||
func installedPackageVersion() string {
|
||||
if !commandExists("dpkg-query") {
|
||||
return ""
|
||||
}
|
||||
cmd := exec.Command("dpkg-query", "-W", "-f=${Status}\n${Version}", "citizen-launcher")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
|
||||
if len(lines) < 2 || !strings.Contains(lines[0], "install ok installed") {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(lines[len(lines)-1])
|
||||
}
|
||||
|
||||
func (a *App) releaseAssetForMode(release githubRelease, version, mode string) (githubAsset, error) {
|
||||
if mode == "deb" {
|
||||
want := "citizen-launcher_" + version + "_amd64.deb"
|
||||
for _, asset := range release.Assets {
|
||||
if asset.Name == want {
|
||||
return asset, nil
|
||||
}
|
||||
}
|
||||
// Accept Debian revisions such as 0.9.2-1 while keeping the package name strict.
|
||||
re := regexp.MustCompile(`^citizen-launcher_` + regexp.QuoteMeta(version) + `(?:-[0-9]+)?_amd64\.deb$`)
|
||||
for _, asset := range release.Assets {
|
||||
if re.MatchString(asset.Name) {
|
||||
return asset, nil
|
||||
}
|
||||
}
|
||||
return githubAsset{}, fmt.Errorf("release %s has no amd64 Debian package", release.TagName)
|
||||
}
|
||||
want := "citizen-launcher-" + version + "-linux-amd64.tar.gz"
|
||||
for _, asset := range release.Assets {
|
||||
if asset.Name == want {
|
||||
return asset, nil
|
||||
}
|
||||
}
|
||||
return githubAsset{}, fmt.Errorf("release %s has no generic amd64 tarball", release.TagName)
|
||||
}
|
||||
|
||||
func (a *App) applySelfUpdate(system, quiet bool) error {
|
||||
st, err := a.selfUpdateStatus(true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if st.State == "current" || st.State == "restart-required" {
|
||||
if !quiet {
|
||||
fmt.Printf("Citizen Launcher %s ist bereits installiert.\n", st.Installed)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if st.State != "available" {
|
||||
return fmt.Errorf("self-update is not applicable in state %q", st.State)
|
||||
}
|
||||
|
||||
release, err := githubLatest(effectiveReleaseRepo())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
asset, err := a.releaseAssetForMode(release, st.Latest, st.Mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !strings.HasPrefix(strings.ToLower(asset.Digest), "sha256:") {
|
||||
return errors.New("release asset has no GitHub SHA-256 digest; refusing automatic update")
|
||||
}
|
||||
|
||||
if st.Mode == "deb" {
|
||||
if !system || os.Geteuid() != 0 {
|
||||
if commandExists("pkexec") {
|
||||
self := a.selfPath
|
||||
if self == "" {
|
||||
self = "/usr/bin/citizen-launcher"
|
||||
}
|
||||
cmd := exec.Command("pkexec", self, "self-update", "apply", "--system")
|
||||
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
return errors.New("Debian package update needs root privileges and pkexec is unavailable")
|
||||
}
|
||||
return a.applyDebUpdate(asset, st.Latest, quiet)
|
||||
}
|
||||
return a.applyUserUpdate(asset, st.Latest, quiet)
|
||||
}
|
||||
|
||||
func (a *App) applyDebUpdate(asset githubAsset, version string, quiet bool) error {
|
||||
cache := "/var/cache/citizen-launcher"
|
||||
if err := os.MkdirAll(cache, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
path := filepath.Join(cache, asset.Name)
|
||||
tmp := path + ".new"
|
||||
_ = os.Remove(tmp)
|
||||
if err := download(asset.BrowserDownloadURL, tmp); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := verifyReleaseDigest(tmp, asset.Digest); err != nil {
|
||||
_ = os.Remove(tmp)
|
||||
return err
|
||||
}
|
||||
if err := verifyDebPackage(tmp, version); err != nil {
|
||||
_ = os.Remove(tmp)
|
||||
return err
|
||||
}
|
||||
if err := os.Rename(tmp, path); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := exec.Command("apt-get",
|
||||
"-o", "DPkg::Lock::Timeout=120",
|
||||
"-o", "Dpkg::Options::=--force-confold",
|
||||
"install", "-y", "--no-install-recommends", path,
|
||||
)
|
||||
cmd.Env = append(os.Environ(), "DEBIAN_FRONTEND=noninteractive")
|
||||
if quiet {
|
||||
cmd.Stdout = a.logWriter()
|
||||
cmd.Stderr = a.logWriter()
|
||||
} else {
|
||||
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
|
||||
}
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("APT package update failed: %w", err)
|
||||
}
|
||||
installed := installedPackageVersion()
|
||||
if compareVersions(installed, version) < 0 {
|
||||
return fmt.Errorf("package manager returned success but installed version is %q, expected at least %q", installed, version)
|
||||
}
|
||||
if !quiet {
|
||||
fmt.Printf("Citizen Launcher wurde auf %s aktualisiert. Ein laufendes GUI verwendet die neue Version nach dem nächsten Neustart.\n", installed)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func verifyDebPackage(path, version string) error {
|
||||
if !commandExists("dpkg-deb") {
|
||||
return errors.New("dpkg-deb is unavailable")
|
||||
}
|
||||
cmd := exec.Command("dpkg-deb", "-f", path, "Package", "Version", "Architecture")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot inspect Debian package: %s", formatCommandFailure(err, out))
|
||||
}
|
||||
values := map[string]string{}
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
k, v, ok := strings.Cut(line, ":")
|
||||
if ok {
|
||||
values[strings.TrimSpace(k)] = strings.TrimSpace(v)
|
||||
}
|
||||
}
|
||||
pkg, pkgVersion, arch := values["Package"], values["Version"], values["Architecture"]
|
||||
if pkg == "" || pkgVersion == "" || arch == "" {
|
||||
return fmt.Errorf("unexpected Debian package metadata: %q", strings.TrimSpace(string(out)))
|
||||
}
|
||||
if pkg != "citizen-launcher" {
|
||||
return fmt.Errorf("refusing package %q", pkg)
|
||||
}
|
||||
if compareVersions(pkgVersion, version) < 0 {
|
||||
return fmt.Errorf("package version %q is older than release %q", pkgVersion, version)
|
||||
}
|
||||
if arch != "amd64" {
|
||||
return fmt.Errorf("refusing architecture %q", arch)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) applyUserUpdate(asset githubAsset, version string, quiet bool) error {
|
||||
if a.selfPath == "" {
|
||||
return errors.New("cannot locate running Citizen Launcher executable")
|
||||
}
|
||||
if !strings.HasPrefix(strings.ToLower(asset.Digest), "sha256:") {
|
||||
return errors.New("release asset has no GitHub SHA-256 digest; refusing automatic update")
|
||||
}
|
||||
if err := os.MkdirAll(a.cacheDir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
archive := filepath.Join(a.cacheDir, asset.Name)
|
||||
if err := download(asset.BrowserDownloadURL, archive); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := verifyReleaseDigest(archive, asset.Digest); err != nil {
|
||||
return err
|
||||
}
|
||||
stage, err := os.MkdirTemp(a.cacheDir, "self-update-")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(stage)
|
||||
if err := extractSingleBinaryTarGz(archive, stage); err != nil {
|
||||
return err
|
||||
}
|
||||
candidate := filepath.Join(stage, "citizen-launcher")
|
||||
out, err := exec.Command(candidate, "--version").CombinedOutput()
|
||||
if err != nil || strings.TrimSpace(string(out)) != version {
|
||||
return fmt.Errorf("downloaded launcher version check failed: %s", formatCommandFailure(err, out))
|
||||
}
|
||||
tmp := a.selfPath + ".new"
|
||||
if err := copyFile(candidate, tmp, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Rename(tmp, a.selfPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if !quiet {
|
||||
fmt.Printf("Citizen Launcher wurde auf %s aktualisiert.\n", version)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractSingleBinaryTarGz(path, dir string) error {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
gz, err := gzip.NewReader(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer gz.Close()
|
||||
tr := tar.NewReader(gz)
|
||||
for {
|
||||
h, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if filepath.Base(h.Name) != "citizen-launcher" || h.Typeflag != tar.TypeReg {
|
||||
continue
|
||||
}
|
||||
target := filepath.Join(dir, "citizen-launcher")
|
||||
out, err := os.OpenFile(target, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, copyErr := io.Copy(out, tr)
|
||||
closeErr := out.Close()
|
||||
if copyErr != nil {
|
||||
return copyErr
|
||||
}
|
||||
return closeErr
|
||||
}
|
||||
return errors.New("tarball does not contain citizen-launcher")
|
||||
}
|
||||
|
||||
func compareVersions(a, b string) int {
|
||||
parse := func(v string) []int {
|
||||
v = strings.TrimPrefix(strings.TrimSpace(v), "v")
|
||||
v = strings.SplitN(v, "-", 2)[0]
|
||||
parts := strings.Split(v, ".")
|
||||
out := make([]int, 3)
|
||||
for i := 0; i < len(out) && i < len(parts); i++ {
|
||||
n, _ := strconv.Atoi(parts[i])
|
||||
out[i] = n
|
||||
}
|
||||
return out
|
||||
}
|
||||
aa, bb := parse(a), parse(b)
|
||||
for i := range aa {
|
||||
if aa[i] < bb[i] {
|
||||
return -1
|
||||
}
|
||||
if aa[i] > bb[i] {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func printSelfUpdateKV(s SelfUpdateStatus) {
|
||||
fmt.Printf("current=%s\n", s.Current)
|
||||
fmt.Printf("installed=%s\n", s.Installed)
|
||||
fmt.Printf("latest=%s\n", s.Latest)
|
||||
fmt.Printf("state=%s\n", s.State)
|
||||
fmt.Printf("mode=%s\n", s.Mode)
|
||||
fmt.Printf("repository=%s\n", s.Repository)
|
||||
fmt.Printf("asset=%s\n", s.Asset)
|
||||
fmt.Printf("restart_needed=%t\n", s.RestartNeeded)
|
||||
if s.Error != "" {
|
||||
fmt.Printf("error=%s\n", s.Error)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCompareVersions(t *testing.T) {
|
||||
cases := []struct {
|
||||
a, b string
|
||||
want int
|
||||
}{
|
||||
{"0.9.2", "0.9.1", 1},
|
||||
{"v1.0.0", "0.9.9", 1},
|
||||
{"0.9.2", "0.9.2", 0},
|
||||
{"0.9.1", "0.9.2", -1},
|
||||
{"0.9.2-1", "0.9.2", 0},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
got := compareVersions(tc.a, tc.b)
|
||||
if got != tc.want {
|
||||
t.Fatalf("compareVersions(%q,%q)=%d want %d", tc.a, tc.b, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseAssetForDeb(t *testing.T) {
|
||||
a, _ := newApp()
|
||||
r := githubRelease{TagName: "v0.9.2", Assets: []githubAsset{
|
||||
{Name: "citizen-launcher-0.9.2-linux-amd64.tar.gz"},
|
||||
{Name: "citizen-launcher_0.9.2_amd64.deb", Digest: "sha256:abc"},
|
||||
}}
|
||||
asset, err := a.releaseAssetForMode(r, "0.9.2", "deb")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if asset.Name != "citizen-launcher_0.9.2_amd64.deb" {
|
||||
t.Fatalf("wrong asset: %s", asset.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyDebPackageMetadata(t *testing.T) {
|
||||
if _, err := exec.LookPath("dpkg-deb"); err != nil {
|
||||
t.Skip("dpkg-deb unavailable")
|
||||
}
|
||||
root := t.TempDir()
|
||||
pkg := filepath.Join(root, "pkg")
|
||||
if err := os.MkdirAll(filepath.Join(pkg, "DEBIAN"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
control := "Package: citizen-launcher\nVersion: 0.9.2\nArchitecture: amd64\nMaintainer: test\nDescription: test\n"
|
||||
if err := os.WriteFile(filepath.Join(pkg, "DEBIAN", "control"), []byte(control), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
deb := filepath.Join(root, "citizen-launcher_0.9.2_amd64.deb")
|
||||
cmd := exec.Command("dpkg-deb", "--build", "--root-owner-group", pkg, deb)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("dpkg-deb: %v: %s", err, out)
|
||||
}
|
||||
if err := verifyDebPackage(deb, "0.9.2"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,8 @@ const (
|
||||
// fail under otherwise healthy Wine prefixes.
|
||||
var basePrefixWinetricksVerbs = []string{"arial", "tahoma", "win11"}
|
||||
|
||||
const wineFileAssociationsKey = "HKEY_CURRENT_USER\\Software\\Wine\\FileOpenAssociations"
|
||||
|
||||
type GameConfig struct {
|
||||
Prefix string `json:"prefix"`
|
||||
GameDir string `json:"game_dir"`
|
||||
@@ -697,11 +699,14 @@ func (a *App) ensurePrefixComponents(gc GameConfig) error {
|
||||
|
||||
runner, _ := a.currentRunner()
|
||||
wine := filepath.Join(runner, "bin", "wine")
|
||||
reg := exec.Command(wine, "reg", "add", `HKEY_CURRENT_USER\\Software\\Wine\\FileOpenAssociations`, `/v`, `Enable`, `/d`, `N`, `/f`)
|
||||
// This is a convenience tweak only: prevent Wine from creating host file
|
||||
// associations. It must never make the Star Citizen installation fail.
|
||||
// Use a normal Go string so Wine receives single registry separators.
|
||||
reg := exec.Command(wine, "reg", "add", wineFileAssociationsKey, "/v", "Enable", "/t", "REG_SZ", "/d", "N", "/f")
|
||||
reg.Env = env
|
||||
out, err := reg.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Wine Registry: %s", formatCommandFailure(err, out))
|
||||
out, regErr := reg.CombinedOutput()
|
||||
if regErr != nil {
|
||||
a.logf("non-fatal Wine registry association tweak warning: %s", formatCommandFailure(regErr, out))
|
||||
}
|
||||
_ = writeMeta(filepath.Join(a.vendorDir, "winetricks", "meta.json"), componentMeta{Version: tag, Updated: time.Now().Format(time.RFC3339)})
|
||||
return nil
|
||||
|
||||
@@ -56,3 +56,12 @@ func TestBasePrefixDoesNotRequirePowerShell(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWineRegistryKeyUsesSingleSeparators(t *testing.T) {
|
||||
if strings.Contains(wineFileAssociationsKey, `\\`) {
|
||||
t.Fatalf("registry key contains doubled separators: %q", wineFileAssociationsKey)
|
||||
}
|
||||
if wineFileAssociationsKey != `HKEY_CURRENT_USER\Software\Wine\FileOpenAssociations` {
|
||||
t.Fatalf("unexpected registry key: %q", wineFileAssociationsKey)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ function cls(el,state){el.classList.remove('good','bad','warn');if(state)el.clas
|
||||
function txt(id,v,c){const e=$(id);const value=(v===undefined||v===null||v==='')?'—':String(v);e.textContent=value;e.title=value;cls(e,c)}
|
||||
function primaryFor(h){if(h==='ready')return['STAR CITIZEN STARTEN','launch'];if(h==='hardware-blocked')return['HARDWARE-DETAILS','doctor'];if(h==='repair'||h==='launcher-repair')return['AUTOMATISCH REPARIEREN','repair'];return['EINRICHTEN & STARTKLAR MACHEN','setup']}
|
||||
function setHeroHealth(h){$('hero').dataset.health=h||'setup'}
|
||||
async function refresh(){try{const s=await api('/api/status');current=s;$('version').textContent='v'+s.version;$('platform').textContent=[s.platform.name,s.platform.desktop].filter(Boolean).join(' · ');$('distro').textContent=s.platform.name;$('session').textContent=s.platform.session||'—';const g=s.game;setHeroHealth(g.health);let head='READY FOR SETUP',sub='Citizen Launcher übernimmt Wine, DXVK, Prefix und RSI Launcher.';if(g.health==='ready'){head='FLIGHT READY';sub='Star Citizen ist startbereit. Autopilot hält den Gaming-Stack aktuell.'}else if(g.health==='hardware-blocked'){head='HARDWARE CHECK';sub=g.hardware_reason||'Die Hardware erfüllt eine Voraussetzung nicht.'}else if(g.health==='repair'||g.health==='launcher-repair'){head='REPAIR AVAILABLE';sub='Ein reparierbarer Zustand wurde erkannt. Die Reparatur verändert keine Spieldaten.'}else if(g.health==='install-game'){head='LAUNCHER READY';sub='Der RSI Launcher ist bereit. Melde dich an und installiere Star Citizen.'}$('headline').textContent=head;$('subline').textContent=sub;const [label,act]=primaryFor(g.health);$('primary').textContent=label;$('primary').dataset.action=act;txt('gpu',g.gpu||'nicht eindeutig erkannt',g.hardware==='blocked'?'bad':'good');txt('vulkan',g.vulkan,g.vulkan==='blocked'?'bad':g.vulkan==='ready'?'good':'warn');txt('ram',g.ram_gib+' GiB',g.ram_gib>=16?'good':'warn');txt('wine',g.wine_version||'wird eingerichtet',g.wine_version?'good':'warn');txt('dxvk',g.dxvk_version||'wird eingerichtet',g.dxvk_version?'good':'warn');txt('rsi',g.launcher_state==='ready'?'bereit':'Setup',g.launcher_state==='ready'?'good':'warn');txt('autopilot',g.autopilot?'aktiv':'aus',g.autopilot?'good':'warn');$('raw').textContent=JSON.stringify(s,null,2)}catch(e){$('headline').textContent='BACKEND ERROR';$('subline').textContent=e.message}}
|
||||
async function refresh(){try{const s=await api('/api/status');current=s;$('version').textContent='v'+s.version;$('platform').textContent=[s.platform.name,s.platform.desktop].filter(Boolean).join(' · ');$('distro').textContent=s.platform.name;$('session').textContent=s.platform.session||'—';txt('packageUpdates',s.package_auto_update?'automatisch':'manuell',s.package_auto_update?'good':'warn');const ub=$('updateBanner');if(s.restart_required){ub.classList.remove('hidden');$('updateText').textContent='Installiert ist '+(s.installed_version||'eine neuere Version')+', dieses Fenster läuft noch mit '+s.version+'.'}else{ub.classList.add('hidden')}const g=s.game;setHeroHealth(g.health);let head='READY FOR SETUP',sub='Citizen Launcher übernimmt Wine, DXVK, Prefix und RSI Launcher.';if(g.health==='ready'){head='FLIGHT READY';sub='Star Citizen ist startbereit. Autopilot hält den Gaming-Stack aktuell.'}else if(g.health==='hardware-blocked'){head='HARDWARE CHECK';sub=g.hardware_reason||'Die Hardware erfüllt eine Voraussetzung nicht.'}else if(g.health==='repair'||g.health==='launcher-repair'){head='REPAIR AVAILABLE';sub='Ein reparierbarer Zustand wurde erkannt. Die Reparatur verändert keine Spieldaten.'}else if(g.health==='install-game'){head='LAUNCHER READY';sub='Der RSI Launcher ist bereit. Melde dich an und installiere Star Citizen.'}$('headline').textContent=head;$('subline').textContent=sub;const [label,act]=primaryFor(g.health);$('primary').textContent=label;$('primary').dataset.action=act;txt('gpu',g.gpu||'nicht eindeutig erkannt',g.hardware==='blocked'?'bad':'good');txt('vulkan',g.vulkan,g.vulkan==='blocked'?'bad':g.vulkan==='ready'?'good':'warn');txt('ram',g.ram_gib+' GiB',g.ram_gib>=16?'good':'warn');txt('wine',g.wine_version||'wird eingerichtet',g.wine_version?'good':'warn');txt('dxvk',g.dxvk_version||'wird eingerichtet',g.dxvk_version?'good':'warn');txt('rsi',g.launcher_state==='ready'?'bereit':'Setup',g.launcher_state==='ready'?'good':'warn');txt('autopilot',g.autopilot?'aktiv':'aus',g.autopilot?'good':'warn');$('raw').textContent=JSON.stringify(s,null,2)}catch(e){$('headline').textContent='BACKEND ERROR';$('subline').textContent=e.message}}
|
||||
function showJob(state,title,message,raw=''){const box=$('job');box.classList.remove('hidden','error','done');if(state==='error')box.classList.add('error');if(state==='done')box.classList.add('done');$('jobTitle').textContent=title;$('jobText').textContent=message||'';const spin=box.querySelector('.spinner');const symbol=$('jobSymbol');spin.classList.toggle('hidden',state!=='running');symbol.classList.toggle('hidden',state==='running');symbol.textContent=state==='done'?'✓':'!';const details=$('jobDetails');if(raw&&raw!==message){details.classList.remove('hidden');$('jobRaw').textContent=raw}else{details.classList.add('hidden');$('jobRaw').textContent=''}}
|
||||
async function action(name){try{if(name==='launch'){await api('/api/action/launch',{method:'POST'});showJob('done','RSI Launcher gestartet','Der Launcher wurde gestartet.');return}const j=await api('/api/action/'+name,{method:'POST'});showJob('running','Aktion: '+name,'Wird ausgeführt …');poll(j.id)}catch(e){showJob('error','Aktion konnte nicht gestartet werden',e.message,e.message)}}
|
||||
async function action(name){try{if(name==='restart'){await api('/api/action/restart',{method:'POST'});showJob('done','Launcher wird neu gestartet','Die neue Version wird geöffnet …');return}if(name==='launch'){await api('/api/action/launch',{method:'POST'});showJob('done','RSI Launcher gestartet','Der Launcher wurde gestartet.');return}const j=await api('/api/action/'+name,{method:'POST'});showJob('running','Aktion: '+name,'Wird ausgeführt …');poll(j.id)}catch(e){showJob('error','Aktion konnte nicht gestartet werden',e.message,e.message)}}
|
||||
async function poll(id){try{const j=await api('/api/job/'+id);if(j.state==='running'){setTimeout(()=>poll(id),800);return}if(j.state==='done')showJob('done','Abgeschlossen',j.message||'Die Aktion wurde erfolgreich abgeschlossen.');else showJob('error','Aktion fehlgeschlagen',j.message||'Die Aktion konnte nicht abgeschlossen werden.',j.error||'');await refresh()}catch(e){showJob('error','Statusfehler','Der Aktionsstatus konnte nicht gelesen werden.',e.message)}}
|
||||
document.addEventListener('click',e=>{const button=e.target.closest('[data-action],[data-open]');if(!button)return;const a=button.dataset.action;if(a)action(a);const o=button.dataset.open;if(o)api('/api/open/'+o,{method:'POST'});});$('toggle').onclick=()=>$('advanced').classList.toggle('hidden');$('primary').onclick=()=>action($('primary').dataset.action);refresh();setInterval(refresh,10000);
|
||||
@@ -3,11 +3,12 @@
|
||||
<title>Citizen Launcher</title><link rel="stylesheet" href="__BASE__/assets/style.css"></head>
|
||||
<body><div class="scan"></div><main>
|
||||
<header><div class="mark">✦</div><div class="brand"><div class="eyebrow">LINUX FLIGHT SYSTEM</div><h1>Citizen Launcher</h1><p id="platform">Linux wird erkannt …</p></div><div class="version" id="version"></div></header>
|
||||
<section class="update-banner hidden" id="updateBanner"><div><b>Neue Version installiert</b><p id="updateText">Citizen Launcher wurde im Hintergrund aktualisiert.</p></div><button data-action="restart">LAUNCHER NEU STARTEN</button></section>
|
||||
<section class="hero" id="hero"><div class="hero-copy"><div class="eyebrow">SYSTEM STATE</div><h2 id="headline">SYSTEM CHECK</h2><p id="subline">Hardware und Gaming-Stack werden geprüft.</p></div><button class="primary" id="primary">PRÜFEN …</button></section>
|
||||
<section class="dashboard">
|
||||
<article class="card hardware"><h3>Hardware</h3><div class="metric"><span>GPU</span><b id="gpu">—</b></div><div class="metric"><span>Vulkan</span><b id="vulkan">—</b></div><div class="metric"><span>RAM</span><b id="ram">—</b></div></article>
|
||||
<article class="card"><h3>Gaming Stack</h3><div class="metric"><span>Wine</span><b id="wine">—</b></div><div class="metric"><span>DXVK</span><b id="dxvk">—</b></div><div class="metric"><span>Launcher</span><b id="rsi">—</b></div></article>
|
||||
<article class="card"><h3>Autopilot</h3><div class="metric"><span>Wartung</span><b id="autopilot">—</b></div><div class="metric"><span>Distribution</span><b id="distro">—</b></div><div class="metric"><span>Session</span><b id="session">—</b></div></article>
|
||||
<article class="card"><h3>Autopilot</h3><div class="metric"><span>Gaming-Stack</span><b id="autopilot">—</b></div><div class="metric"><span>Launcher-Updates</span><b id="packageUpdates">—</b></div><div class="metric"><span>Distribution</span><b id="distro">—</b></div><div class="metric"><span>Session</span><b id="session">—</b></div></article>
|
||||
</section>
|
||||
<section class="actions"><button data-action="maintain"><strong>Stack aktualisieren</strong><small>Wine, DXVK & Launcher prüfen</small></button><button data-action="repair"><strong>Automatisch reparieren</strong><small>Setup und Launcher heilen</small></button><button data-action="doctor"><strong>Wine-Selbsttest</strong><small>Prefix isoliert prüfen</small></button><button data-action="support"><strong>Support-Paket</strong><small>Diagnose für Hilfe erstellen</small></button></section>
|
||||
<section class="job hidden" id="job"><div class="job-icon"><div class="spinner"></div><span class="job-symbol hidden" id="jobSymbol">!</span></div><div class="job-body"><b id="jobTitle">Aktion läuft</b><p id="jobText"></p><details class="job-details hidden" id="jobDetails"><summary>Technische Details</summary><pre id="jobRaw"></pre></details></div></section>
|
||||
|
||||
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,40 @@
|
||||
# Automatic launcher updates
|
||||
|
||||
## Debian / Ubuntu / Mint
|
||||
|
||||
The `.deb` installs a root systemd timer:
|
||||
|
||||
- `citizen-launcher-self-update.timer`
|
||||
- checks on boot and every six hours
|
||||
- reads the latest GitHub Release from the configured repository
|
||||
- accepts only a newer `citizen-launcher_<version>_amd64.deb`
|
||||
- requires GitHub's `sha256:` release asset digest
|
||||
- verifies the downloaded package name, version, and architecture with `dpkg-deb`
|
||||
- installs via `apt-get` with the dpkg lock timeout enabled
|
||||
|
||||
The release repository is stored in:
|
||||
|
||||
```text
|
||||
/etc/citizen-launcher/release-repo
|
||||
```
|
||||
|
||||
Default for this project:
|
||||
|
||||
```text
|
||||
sendnwv/omarchy-sc
|
||||
```
|
||||
|
||||
A package update does not forcibly terminate a running GUI. The existing GUI detects
|
||||
that `/usr/bin/citizen-launcher` is newer and offers **Launcher neu starten**.
|
||||
|
||||
## Generic user install
|
||||
|
||||
The Autopilot user timer checks GitHub releases. For a `~/.local` installation it
|
||||
can replace the launcher binary atomically from the verified Linux tarball without
|
||||
root privileges.
|
||||
|
||||
## Release publishing
|
||||
|
||||
`.github/workflows/release.yml` builds and publishes the `.deb`, generic tarball and
|
||||
SHA256SUMS whenever a `v<VERSION>` tag is pushed. GitHub computes an immutable asset
|
||||
digest that the Debian self-updater verifies before installation.
|
||||
+10
-3
@@ -1,10 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
VERSION="0.9.1"; ARCH="amd64"; WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT
|
||||
VERSION="$(tr -d '[:space:]' < "$ROOT/VERSION")"; ARCH="amd64"; WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT
|
||||
PKG="$WORK/citizen-launcher_${VERSION}_${ARCH}"
|
||||
mkdir -p "$PKG/DEBIAN" "$PKG/usr/bin" "$PKG/usr/share/applications"
|
||||
mkdir -p "$PKG/DEBIAN" "$PKG/usr/bin" "$PKG/usr/share/applications" "$PKG/usr/lib/systemd/system" "$PKG/etc/citizen-launcher"
|
||||
install -m755 "$ROOT/backend/bin/citizen-launcher" "$PKG/usr/bin/citizen-launcher"
|
||||
|
||||
install -m644 "$ROOT/packaging/systemd/citizen-launcher-self-update.service" "$PKG/usr/lib/systemd/system/citizen-launcher-self-update.service"
|
||||
install -m644 "$ROOT/packaging/systemd/citizen-launcher-self-update.timer" "$PKG/usr/lib/systemd/system/citizen-launcher-self-update.timer"
|
||||
printf '%s\n' "${CITIZEN_LAUNCHER_RELEASE_REPO:-sendnwv/omarchy-sc}" > "$PKG/etc/citizen-launcher/release-repo"
|
||||
install -m755 "$ROOT/packaging/postinst" "$PKG/DEBIAN/postinst"
|
||||
install -m755 "$ROOT/packaging/prerm" "$PKG/DEBIAN/prerm"
|
||||
install -m755 "$ROOT/packaging/postrm" "$PKG/DEBIAN/postrm"
|
||||
cat > "$PKG/DEBIAN/control" <<EOF
|
||||
Package: citizen-launcher
|
||||
Version: $VERSION
|
||||
@@ -12,7 +19,7 @@ Section: games
|
||||
Priority: optional
|
||||
Architecture: $ARCH
|
||||
Maintainer: Citizen Launcher Project
|
||||
Depends: ca-certificates, tar, xdg-utils, curl, cabextract, unzip, xz-utils
|
||||
Depends: ca-certificates, tar, xdg-utils, curl, cabextract, unzip, xz-utils, apt
|
||||
Recommends: pciutils, vulkan-tools
|
||||
Description: Star Citizen setup, launcher and self-maintaining gaming stack for Linux
|
||||
Distro-neutral Citizen Launcher with Wine/DXVK/RSI management and local GUI.
|
||||
|
||||
@@ -2,4 +2,5 @@
|
||||
set -euo pipefail
|
||||
ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
mkdir -p "$ROOT/dist"
|
||||
tar -C "$ROOT/backend/bin" -czf "$ROOT/dist/citizen-launcher-0.9.1-linux-amd64.tar.gz" citizen-launcher
|
||||
VERSION="$(tr -d '[:space:]' < "$ROOT/VERSION")"
|
||||
tar -C "$ROOT/backend/bin" -czf "$ROOT/dist/citizen-launcher-${VERSION}-linux-amd64.tar.gz" citizen-launcher
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: citizen-launcher
|
||||
Version: 0.9.1
|
||||
Version: 0.9.2
|
||||
Release: 1%{?dist}
|
||||
Summary: Star Citizen launcher and self-maintaining Wine stack for Linux
|
||||
License: MIT
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||
systemctl enable --now citizen-launcher-self-update.timer >/dev/null 2>&1 || true
|
||||
fi
|
||||
exit 0
|
||||
@@ -0,0 +1,6 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||
fi
|
||||
exit 0
|
||||
@@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
if [ "$1" = remove ] || [ "$1" = deconfigure ]; then
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl disable --now citizen-launcher-self-update.timer >/dev/null 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
exit 0
|
||||
@@ -0,0 +1,16 @@
|
||||
[Unit]
|
||||
Description=Citizen Launcher automatic package update
|
||||
Documentation=https://github.com/sendnwv/omarchy-sc
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
ConditionPathExists=/usr/bin/citizen-launcher
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/bin/citizen-launcher self-update apply --system --quiet
|
||||
Nice=10
|
||||
IOSchedulingClass=best-effort
|
||||
IOSchedulingPriority=7
|
||||
PrivateTmp=true
|
||||
ProtectHome=true
|
||||
UMask=0022
|
||||
@@ -0,0 +1,12 @@
|
||||
[Unit]
|
||||
Description=Check Citizen Launcher package updates automatically
|
||||
|
||||
[Timer]
|
||||
OnBootSec=5min
|
||||
OnUnitActiveSec=6h
|
||||
RandomizedDelaySec=20min
|
||||
Persistent=true
|
||||
Unit=citizen-launcher-self-update.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
+1
-1
@@ -3,7 +3,7 @@ set -euo pipefail
|
||||
ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
for f in build.sh install.sh install-omarchy.sh uninstall.sh packaging/build-deb.sh packaging/build-tarball.sh integrations/omarchy/citizenctl; do bash -n "$ROOT/$f"; done
|
||||
(cd "$ROOT/backend" && gofmt -w cmd/citizen-launcher/*.go && go test ./... && go vet ./... && ./build.sh)
|
||||
"$ROOT/backend/bin/citizen-launcher" --version | grep -qx '0.9.1'
|
||||
"$ROOT/backend/bin/citizen-launcher" --version | grep -qx "$(tr -d '[:space:]' < "$ROOT/VERSION")"
|
||||
"$ROOT/backend/bin/citizen-launcher" platform --json | grep -q '"id"'
|
||||
# Ensure GUI assets are embedded by looking for a distinctive string in binary.
|
||||
grep -a -q 'FLIGHT READY' "$ROOT/backend/bin/citizen-launcher"
|
||||
|
||||
Reference in New Issue
Block a user