mirror of
https://github.com/lucannez64/darling-lol.git
synced 2026-10-26 19:47:42 +00:00
fix(launcher): only remove launcher artifacts this project created
Match the legacy entry against this checkout's desktop-run.sh instead of any desktop-run.sh, and delete the legacy icon files only after that entry or the old artwork proves ownership. Quote and escape Exec arguments as the Desktop Entry specification requires, so a checkout path with reserved characters no longer produces an invalid entry, and escape the Path value. Use StartupWMClass=RiotClientServices in both entries: Darling derives WM_CLASS from -[NSProcessInfo processName], the basename of argv[0], and the launcher starts .../RiotClientServices. "Riot Client" is only the bundle display name, so the checkout entry never matched the window. Both entries now share Name/GenericName/Comment/Icon/Keywords/Categories and a test keeps them in sync; Terminal stays different because the checkout entry logs to build/desktop-launch.log while the packaged entry runs the session in a terminal. Also drop the stray top-level icon copies, handle dangling legacy symlinks, name the icon for assistive technology, document the alias and menu entry, and cover the helper with disposable-HOME tests.
This commit is contained in:
1 parent
0947a9f20d
commit
e00ee99522
8 files changed
+286
-46
No files matched your search
@@ -25,6 +25,7 @@ jobs:
|
||||
- run: python -m unittest discover -s tests -p 'test_nix_repairs.py'
|
||||
- run: python -m unittest discover -s tests -p 'test_hidden_riot_recovery.py'
|
||||
- run: python -m unittest discover -s tests -p 'test_desktop_run.py'
|
||||
- run: python -m unittest discover -s tests -p 'test_desktop_entry.py'
|
||||
- run: python -m unittest discover -s tests -p 'test_urlsession_runner.py'
|
||||
- run: python -m unittest discover -s tests -p 'test_system_profiler.py'
|
||||
- run: python -m unittest discover -s tests -p 'test_metal_device_info.py'
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" role="img" aria-label="darling-lol">
|
||||
<title>darling-lol</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" role="img" aria-label="Darling Launcher">
|
||||
<title>Darling Launcher</title>
|
||||
<rect x="8" y="8" width="112" height="112" rx="26" fill="#0f172a"/>
|
||||
<path fill-rule="evenodd" fill="#2dd4bf" d="M38 30h20c21 0 34 15 34 34s-13 34-34 34H38V30zm18 18v32h2c9 0 16-7 16-16s-7-16-16-16h-2z"/>
|
||||
<rect x="30" y="90" width="68" height="8" rx="4" fill="#818cf8"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 411 B After Width: | Height: | Size: 421 B |
@@ -91,6 +91,9 @@ darling-lol --installer
|
||||
darling-lol-shell
|
||||
```
|
||||
|
||||
The application menu entry is `Darling Launcher`, and `darling-launcher` is an
|
||||
alias for `darling-lol` for scripts that prefer the neutral name.
|
||||
|
||||
If the desktop entry does not appear, ensure your desktop session includes
|
||||
`~/.nix-profile/share` in `XDG_DATA_DIRS`, then log out and back in. Non-NixOS
|
||||
systems may need this desktop-session integration after installing Nix.
|
||||
|
||||
+9
-4
@@ -61,10 +61,15 @@ The normal launcher starts neither an Electron inspector nor client API/UI helpe
|
||||
|
||||
## Application-menu diagnostics and recovery options
|
||||
|
||||
`scripts/create-desktop-entry.sh` creates an application-menu entry that runs
|
||||
`scripts/desktop-run.sh`, preserving the normal launcher behavior and saving
|
||||
stdout/stderr in `build/desktop-launch.log` with mode 0600. Logs can contain
|
||||
private session details; do not attach raw launcher logs to a public issue.
|
||||
`scripts/create-desktop-entry.sh` creates a `Darling Launcher` application-menu
|
||||
entry that runs `scripts/desktop-run.sh`, preserving the normal launcher behavior
|
||||
and saving stdout/stderr in `build/desktop-launch.log` with mode 0600. Logs can
|
||||
contain private session details; do not attach raw launcher logs to a public
|
||||
issue. The helper also removes the `league-of-legends.*` entry and icons written
|
||||
by older versions, but only when it can prove this project created them. The
|
||||
entry keeps `StartupWMClass=RiotClientServices`: Darling derives `WM_CLASS` from
|
||||
the basename of `argv[0]`, so that is the window class of the client the entry
|
||||
starts, while `Riot Client` is only its bundle display name.
|
||||
Region/locale remain the existing `RCLOL_REGION`/`RCLOL_LOCALE` options.
|
||||
|
||||
`RCLOL_CDN_IPV4_ONLY` defaults to `1` in the Riot launcher and can be set to `0`
|
||||
|
||||
+5
-1
@@ -78,6 +78,7 @@ Host-only checks, which do not start Darling:
|
||||
python3 tools/riot-cdn-dns-offline-check.py
|
||||
python3 -m unittest discover -s tests -p 'test_hidden_riot_recovery.py'
|
||||
python3 -m unittest discover -s tests -p 'test_desktop_run.py'
|
||||
python3 -m unittest discover -s tests -p 'test_desktop_entry.py'
|
||||
python3 -m unittest discover -s tests -p 'test_urlsession_runner.py'
|
||||
```
|
||||
|
||||
@@ -87,7 +88,10 @@ insensitivity, A queries, non-CDN names and misleading domain suffixes without
|
||||
sending a DNS query. Recovery tests use fake `/proc` entries and mock every
|
||||
compositor/shutdown command. The desktop test checks exit/argument forwarding,
|
||||
append-only output and tightening an existing public log to mode 0600. The
|
||||
runner test uses a fake `darling` executable to verify loopback fixtures and
|
||||
desktop-entry test renders the helper against a disposable `HOME` and checks
|
||||
the neutral fields, Exec quoting, icon paths, that only launcher artifacts
|
||||
this project created are removed, and that the checkout and packaged entries
|
||||
share their identity fields. The runner test uses a fake `darling` executable to verify loopback fixtures and
|
||||
private-prefix-only shutdown/cleanup.
|
||||
|
||||
With the pinned Darling sources available, run the host resolver regression:
|
||||
|
||||
@@ -339,6 +339,10 @@ DESKTOP_EOF
|
||||
grep -q '^Name=Darling Launcher$' ${darlingLol}/share/applications/darling-launcher.desktop
|
||||
grep -q '^Icon=darling-launcher$' ${darlingLol}/share/applications/darling-launcher.desktop
|
||||
grep -q '^Keywords=Darling;Launcher;Compatibility;$' ${darlingLol}/share/applications/darling-launcher.desktop
|
||||
grep -q '^Categories=Game;$' ${darlingLol}/share/applications/darling-launcher.desktop
|
||||
# Darling derives WM_CLASS from argv[0], so the window class is the
|
||||
# client executable name; this is an interoperability identifier.
|
||||
grep -q '^StartupWMClass=RiotClientServices$' ${darlingLol}/share/applications/darling-launcher.desktop
|
||||
! grep -qiE '^(Name|Comment|GenericName|Keywords)=.*(League|LoL|Riot|Vanguard|anti-cheat)' ${darlingLol}/share/applications/darling-launcher.desktop
|
||||
test -x ${darlingLol}/bin/darling-launcher
|
||||
touch "$out"
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
"""Verify the desktop-entry helper against a disposable HOME."""
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
HELPER = ROOT / 'tools/desktop-entry.py'
|
||||
FLAKE = ROOT / 'flake.nix'
|
||||
OLD_SVG = '<svg><linearGradient id="goldGrad"/><path d="M172 108 L242 108"/></svg>'
|
||||
IDENTITY_KEYS = ('Name', 'GenericName', 'Comment', 'Icon', 'Keywords', 'Categories', 'StartupWMClass')
|
||||
|
||||
|
||||
def parse_entry(text):
|
||||
entry = {}
|
||||
for line in text.splitlines():
|
||||
if '=' in line and not line.startswith(('#', '[')):
|
||||
key, value = line.split('=', 1)
|
||||
entry[key] = value
|
||||
return entry
|
||||
|
||||
|
||||
class DesktopEntryTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory(prefix='desktop entry ', dir=os.environ.get('TMPDIR'))
|
||||
self.addCleanup(self.tmp.cleanup)
|
||||
self.root = Path(self.tmp.name)
|
||||
self.home = self.root / 'home'
|
||||
self.home.mkdir()
|
||||
|
||||
def project(self, name='project'):
|
||||
project = self.root / name
|
||||
project.mkdir(parents=True)
|
||||
shutil.copy2(ROOT / 'darling-lol.svg', project / 'darling-lol.svg')
|
||||
shutil.copy2(ROOT / 'darling-lol.png', project / 'darling-lol.png')
|
||||
return project
|
||||
|
||||
def install(self, project):
|
||||
script = project / 'desktop-entry.py'
|
||||
script.write_text(HELPER.read_text()
|
||||
.replace('@PROJECT_ROOT@', str(project))
|
||||
.replace('@HOST_HOME@', str(self.home)))
|
||||
return subprocess.run([sys.executable, str(script)], capture_output=True, text=True,
|
||||
env=dict(os.environ, HOME=str(self.home)), check=False)
|
||||
|
||||
def entry(self):
|
||||
return self.home / '.local/share/applications/darling-launcher.desktop'
|
||||
|
||||
def icons(self):
|
||||
base = self.home / '.local/share/icons'
|
||||
return base / 'hicolor/scalable/apps/darling-launcher.svg', base / 'hicolor/128x128/apps/darling-launcher.png'
|
||||
|
||||
def test_installs_neutral_entry_and_icons(self):
|
||||
project = self.project()
|
||||
result = self.install(project)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
text = self.entry().read_text()
|
||||
self.assertIn('Name=Darling Launcher\n', text)
|
||||
self.assertIn('Icon=darling-launcher\n', text)
|
||||
self.assertIn(f'Path={project}\n', text)
|
||||
for field in ('Name', 'Comment', 'GenericName', 'Keywords'):
|
||||
line = next(line for line in text.splitlines() if line.startswith(field + '='))
|
||||
for brand in ('League', 'LoL', 'Riot', 'Vanguard', 'anti-cheat'):
|
||||
self.assertNotIn(brand.lower(), line.lower())
|
||||
svg, png = self.icons()
|
||||
self.assertTrue(svg.is_file())
|
||||
self.assertTrue(png.is_file())
|
||||
self.assertEqual(png.read_bytes(), (project / 'darling-lol.png').read_bytes())
|
||||
|
||||
def test_preserves_entries_and_icons_it_did_not_create(self):
|
||||
project = self.project()
|
||||
other = self.root / 'elsewhere'
|
||||
(self.home / '.local/share/applications').mkdir(parents=True)
|
||||
legacy_entry = self.home / '.local/share/applications/league-of-legends.desktop'
|
||||
legacy_entry.write_text(f'[Desktop Entry]\nExec={other}/scripts/desktop-run.sh\n')
|
||||
(self.home / 'Desktop').mkdir()
|
||||
desktop_entry = self.home / 'Desktop/league-of-legends.desktop'
|
||||
desktop_entry.write_text(f'[Desktop Entry]\nExec={other}/scripts/desktop-run.sh\n')
|
||||
icons = self.home / '.local/share/icons'
|
||||
icons.mkdir(parents=True)
|
||||
(icons / 'league-of-legends.png').write_text('user icon')
|
||||
result = self.install(project)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertTrue(legacy_entry.is_file())
|
||||
self.assertTrue(desktop_entry.is_file())
|
||||
self.assertEqual((icons / 'league-of-legends.png').read_text(), 'user icon')
|
||||
self.assertTrue(self.entry().is_file())
|
||||
|
||||
def test_removes_legacy_artifacts_it_created(self):
|
||||
project = self.project()
|
||||
applications = self.home / '.local/share/applications'
|
||||
applications.mkdir(parents=True)
|
||||
owned = f'[Desktop Entry]\nExec="{project}/scripts/desktop-run.sh"\n'
|
||||
legacy_entry = applications / 'league-of-legends.desktop'
|
||||
legacy_entry.write_text(owned)
|
||||
(self.home / 'Desktop').mkdir()
|
||||
desktop_link = self.home / 'Desktop/league-of-legends.desktop'
|
||||
desktop_link.symlink_to(project / 'scripts/desktop-run.sh')
|
||||
icons = self.home / '.local/share/icons'
|
||||
scalable = icons / 'hicolor/scalable/apps'
|
||||
legacy_png_dir = icons / 'hicolor/512x512/apps'
|
||||
scalable.mkdir(parents=True)
|
||||
legacy_png_dir.mkdir(parents=True)
|
||||
legacy_svg = scalable / 'league-of-legends.svg'
|
||||
legacy_svg.write_text(OLD_SVG)
|
||||
legacy_png = legacy_png_dir / 'league-of-legends.png'
|
||||
legacy_png.write_bytes(b'old client icon')
|
||||
result = self.install(project)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertFalse(legacy_entry.exists())
|
||||
self.assertFalse(desktop_link.is_symlink())
|
||||
self.assertFalse(legacy_svg.exists())
|
||||
self.assertFalse(legacy_png.exists())
|
||||
self.assertTrue(self.entry().is_file())
|
||||
|
||||
def test_quotes_exec_arguments_with_reserved_characters(self):
|
||||
project = self.project('proj $dollar#hash\'quote')
|
||||
result = self.install(project)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
text = self.entry().read_text()
|
||||
exec_line = next(line for line in text.splitlines() if line.startswith('Exec='))
|
||||
self.assertTrue(exec_line.startswith('Exec="'), exec_line)
|
||||
self.assertTrue(exec_line.endswith('"'), exec_line)
|
||||
self.assertIn('\\$dollar', exec_line)
|
||||
if shutil.which('desktop-file-validate'):
|
||||
check = subprocess.run(['desktop-file-validate', str(self.entry())],
|
||||
capture_output=True, text=True, check=False)
|
||||
self.assertEqual(check.returncode, 0, check.stdout + check.stderr)
|
||||
|
||||
def test_identity_fields_match_the_packaged_entry(self):
|
||||
project = self.project()
|
||||
result = self.install(project)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
rendered = parse_entry(self.entry().read_text())
|
||||
packaged_here = re.search(
|
||||
r"cat << 'DESKTOP_EOF' > \$out/share/applications/darling-launcher\.desktop\n(.*?)\n\s*DESKTOP_EOF",
|
||||
FLAKE.read_text(), re.S)
|
||||
self.assertIsNotNone(packaged_here, 'packaged desktop entry not found in flake.nix')
|
||||
packaged = parse_entry(packaged_here.group(1))
|
||||
for key in IDENTITY_KEYS:
|
||||
self.assertIn(key, packaged)
|
||||
self.assertEqual(rendered.get(key), packaged[key], key)
|
||||
|
||||
def test_missing_icon_fails(self):
|
||||
project = self.project()
|
||||
(project / 'darling-lol.png').unlink()
|
||||
result = self.install(project)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn('launcher icon is missing', result.stderr)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
+105
-39
@@ -8,14 +8,84 @@ from pathlib import Path
|
||||
PROJECT_ROOT = Path("@PROJECT_ROOT@")
|
||||
HOST_HOME = Path("@HOST_HOME@")
|
||||
|
||||
# Marks a launcher entry created by this project. Previous versions pointed
|
||||
# Exec at this checkout's desktop-run.sh, so an entry without that marker
|
||||
# belongs to someone else and is left alone.
|
||||
LEGACY_RUN_SCRIPT = "scripts/desktop-run.sh"
|
||||
|
||||
def remove_legacy_entry(path: Path) -> None:
|
||||
# Distinctive fragments of the artwork the previous helper wrote.
|
||||
LEGACY_SVG_MARKERS = ('id="goldGrad"', 'M172 108 L242 108')
|
||||
|
||||
# Characters that force Exec quoting, per the Desktop Entry specification.
|
||||
EXEC_RESERVED = set(' \t\n"\'\\><~|&;$*?#()`')
|
||||
|
||||
|
||||
def escape_string(value: str) -> str:
|
||||
"""Escape a value of Desktop Entry type ``string``."""
|
||||
return (value.replace("\\", "\\\\").replace("\t", "\\t")
|
||||
.replace("\n", "\\n").replace("\r", "\\r"))
|
||||
|
||||
|
||||
def quote_exec_argument(value: str) -> str:
|
||||
"""Escape and quote one Exec argument as the specification requires."""
|
||||
escaped = value.replace("%", "%%")
|
||||
if not any(char in EXEC_RESERVED for char in escaped):
|
||||
return escaped
|
||||
escaped = "".join("\\" + char if char in '"`$\\' else char for char in escaped)
|
||||
return f'"{escaped}"'
|
||||
|
||||
|
||||
def legacy_entry_owned(path: Path) -> bool:
|
||||
"""Return True when a launcher entry was created by this project."""
|
||||
marker = str(PROJECT_ROOT / LEGACY_RUN_SCRIPT)
|
||||
try:
|
||||
text = path.read_text(errors="ignore")
|
||||
except OSError:
|
||||
# A dangling symlink still counts when it points at this checkout.
|
||||
try:
|
||||
text = os.readlink(path)
|
||||
except OSError:
|
||||
return False
|
||||
return marker in text or marker.replace("%", "%%") in text
|
||||
|
||||
|
||||
def remove_legacy_entry(path: Path) -> bool:
|
||||
"""Remove a previous launcher entry only when this project created it."""
|
||||
if not (path.is_symlink() or path.exists()):
|
||||
return False
|
||||
if not legacy_entry_owned(path):
|
||||
return False
|
||||
try:
|
||||
if path.exists() and "desktop-run.sh" in path.read_text(errors="ignore"):
|
||||
path.unlink()
|
||||
except OSError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def legacy_svg_owned(path: Path) -> bool:
|
||||
"""Return True when an icon is the artwork the previous helper wrote."""
|
||||
try:
|
||||
text = path.read_text(errors="ignore")
|
||||
except OSError:
|
||||
return False
|
||||
return all(marker in text for marker in LEGACY_SVG_MARKERS)
|
||||
|
||||
|
||||
def shortcut_directories() -> list[Path]:
|
||||
"""Return the desktop directories that hold launcher shortcuts."""
|
||||
directories: list[Path] = []
|
||||
try:
|
||||
result = subprocess.run(["xdg-user-dir", "DESKTOP"], capture_output=True, text=True, check=False)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
path = Path(result.stdout.strip())
|
||||
if path.is_dir():
|
||||
directories.append(path)
|
||||
except Exception:
|
||||
pass
|
||||
for candidate in [HOST_HOME / "Desktop"]:
|
||||
if candidate.is_dir() and candidate not in directories:
|
||||
directories.append(candidate)
|
||||
return directories
|
||||
|
||||
|
||||
def main():
|
||||
@@ -35,32 +105,42 @@ def main():
|
||||
|
||||
# Install the project's original icon under a neutral name. Do not extract
|
||||
# or install an icon from the game/client installation.
|
||||
svg_target = scalable_dir / "darling-launcher.svg"
|
||||
png_target = png_dir / "darling-launcher.png"
|
||||
shutil.copyfile(source_svg, svg_target)
|
||||
shutil.copyfile(source_png, png_target)
|
||||
shutil.copyfile(source_svg, icons_base / "darling-launcher.svg")
|
||||
shutil.copyfile(source_png, icons_base / "darling-launcher.png")
|
||||
shutil.copyfile(source_svg, scalable_dir / "darling-launcher.svg")
|
||||
shutil.copyfile(source_png, png_dir / "darling-launcher.png")
|
||||
|
||||
# Remove artifacts created by older versions of this helper.
|
||||
remove_legacy_entry(apps_base / "league-of-legends.desktop")
|
||||
# Remove artifacts created by older versions of this helper. Only files
|
||||
# proven to be ours are deleted: an entry that pointed at this checkout's
|
||||
# desktop-run.sh, and the SVG with the old artwork. The old PNG was
|
||||
# extracted from the client and cannot be verified, so it is removed only
|
||||
# when one of those proofs succeeded.
|
||||
owned = remove_legacy_entry(apps_base / "league-of-legends.desktop")
|
||||
desktop_dirs = shortcut_directories()
|
||||
for directory in desktop_dirs:
|
||||
owned = remove_legacy_entry(directory / "league-of-legends.desktop") or owned
|
||||
legacy_svgs = (scalable_dir / "league-of-legends.svg", icons_base / "league-of-legends.svg")
|
||||
if any(legacy_svg_owned(path) for path in legacy_svgs):
|
||||
owned = True
|
||||
|
||||
if owned:
|
||||
for legacy_icon in (
|
||||
icons_base / "league-of-legends.svg",
|
||||
*legacy_svgs,
|
||||
icons_base / "league-of-legends.png",
|
||||
scalable_dir / "league-of-legends.svg",
|
||||
icons_base / "hicolor/512x512/apps/league-of-legends.png",
|
||||
):
|
||||
try:
|
||||
if legacy_icon.exists():
|
||||
legacy_icon.unlink()
|
||||
legacy_icon.unlink(missing_ok=True)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# Create the desktop entry. Keep StartupWMClass: it is the client's real
|
||||
# window class and is required for the compositor to match the window.
|
||||
run_script = PROJECT_ROOT / "scripts/desktop-run.sh"
|
||||
exec_path = str(run_script).replace("%", "%%")
|
||||
exec_cmd = f'"{exec_path}"' if " " in exec_path else exec_path
|
||||
# Create the desktop entry. Keep StartupWMClass: Darling's AppKit sets
|
||||
# WM_CLASS from -[NSProcessInfo processName], which is the basename of
|
||||
# argv[0], and the launcher starts .../RiotClientServices. It is an
|
||||
# interoperability identifier, not a user-visible name.
|
||||
# Terminal stays false here because this entry runs desktop-run.sh, which
|
||||
# saves session output in build/desktop-launch.log; the packaged entry runs
|
||||
# the session directly and therefore needs a terminal.
|
||||
exec_cmd = quote_exec_argument(str(PROJECT_ROOT / LEGACY_RUN_SCRIPT))
|
||||
project_path = escape_string(str(PROJECT_ROOT))
|
||||
desktop_content = f"""[Desktop Entry]
|
||||
Version=1.0
|
||||
Type=Application
|
||||
@@ -68,12 +148,12 @@ Name=Darling Launcher
|
||||
GenericName=Game
|
||||
Comment=Unofficial launcher for the macOS game client via Darling
|
||||
Exec={exec_cmd}
|
||||
Path={PROJECT_ROOT}
|
||||
Path={project_path}
|
||||
Icon=darling-launcher
|
||||
Terminal=false
|
||||
Categories=Game;ActionGame;StrategyGame;
|
||||
Keywords=darling;launcher;compatibility;
|
||||
StartupWMClass=Riot Client
|
||||
Categories=Game;
|
||||
Keywords=Darling;Launcher;Compatibility;
|
||||
StartupWMClass=RiotClientServices
|
||||
StartupNotify=false
|
||||
"""
|
||||
|
||||
@@ -82,22 +162,8 @@ StartupNotify=false
|
||||
os.chmod(desktop_file, 0o755)
|
||||
print(f"Created application launcher: {desktop_file}")
|
||||
|
||||
target_desktop_dirs = []
|
||||
try:
|
||||
res = subprocess.run(["xdg-user-dir", "DESKTOP"], capture_output=True, text=True, check=False)
|
||||
if res.returncode == 0 and res.stdout.strip():
|
||||
d = Path(res.stdout.strip())
|
||||
if d.is_dir() and d not in target_desktop_dirs:
|
||||
target_desktop_dirs.append(d)
|
||||
except Exception:
|
||||
pass
|
||||
for cand in [HOST_HOME / "Desktop"]:
|
||||
if cand.is_dir() and cand not in target_desktop_dirs:
|
||||
target_desktop_dirs.append(cand)
|
||||
|
||||
for ddir in target_desktop_dirs:
|
||||
remove_legacy_entry(ddir / "league-of-legends.desktop")
|
||||
desktop_shortcut = ddir / "darling-launcher.desktop"
|
||||
for directory in desktop_dirs:
|
||||
desktop_shortcut = directory / "darling-launcher.desktop"
|
||||
desktop_shortcut.write_text(desktop_content)
|
||||
os.chmod(desktop_shortcut, 0o755)
|
||||
gio = shutil.which("gio")
|
||||
|
||||
Reference in new issue
Block a user