diff --git a/flake.nix b/flake.nix index ef8a7b5..cc37e30 100644 --- a/flake.nix +++ b/flake.nix @@ -1,5 +1,5 @@ { - description = "Unofficial Darling League of Legends - experimental macOS LoL runner on Linux via Darling"; + description = "Unofficial Darling launcher for the macOS game client on Linux"; inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; @@ -136,7 +136,7 @@ pythonEnv ]; - # FHS environment for building and running Darling and League of Legends. + # FHS environment for building and running Darling and the macOS game client. # Darling requires root/multitasking mount & namespace semantics, /usr/local layout, # and dynamically compiled overlays. An FHS environment provides the standard standard # filesystem hierarchies (/usr/include, /usr/lib, /usr/bin) and native dynamic linker. @@ -223,10 +223,10 @@ mkdir -p $out/bin $out/share/darling-lol $out/share/icons/hicolor/scalable/apps $out/share/icons/hicolor/128x128/apps $out/share/pixmaps $out/share/applications cp -r * $out/share/darling-lol/ - cp darling-lol.svg $out/share/icons/hicolor/scalable/apps/darling-lol.svg - cp darling-lol.png $out/share/icons/hicolor/128x128/apps/darling-lol.png - cp darling-lol.png $out/share/pixmaps/darling-lol.png - cp darling-lol.svg $out/share/pixmaps/darling-lol.svg + cp darling-lol.svg $out/share/icons/hicolor/scalable/apps/darling-launcher.svg + cp darling-lol.png $out/share/icons/hicolor/128x128/apps/darling-launcher.png + cp darling-lol.png $out/share/pixmaps/darling-launcher.png + cp darling-lol.svg $out/share/pixmaps/darling-launcher.svg # Install configured launcher substitute ./darling-lol-launcher.sh $out/bin/darling-lol \ @@ -235,6 +235,7 @@ --replace-fail "@UNSHARE_BIN@" "${pkgs.util-linux}/bin/unshare" chmod +x $out/bin/darling-lol wrapProgram $out/bin/darling-lol --prefix PATH : ${pkgs.lib.makeBinPath [ pythonEnv pkgs.coreutils pkgs.util-linux pkgs.gawk ]} + ln -s darling-lol $out/bin/darling-launcher makeWrapper $out/bin/darling-lol $out/bin/darling-lol-installer \ --add-flags "--installer" @@ -245,15 +246,15 @@ makeWrapper $out/bin/darling-lol $out/bin/darling-lol-build \ --add-flags "./scripts/build.sh" - cat << 'DESKTOP_EOF' > $out/share/applications/darling-lol.desktop + cat << 'DESKTOP_EOF' > $out/share/applications/darling-launcher.desktop [Desktop Entry] Categories=Game; -Comment=Unofficial League of Legends macOS client compatibility layer for Linux via Darling -Exec=darling-lol -GenericName=MOBA Game -Icon=darling-lol -Keywords=League;LoL;Riot;Darling; -Name=League of Legends (unofficial) +Comment=Unofficial launcher for the macOS game client via Darling +Exec=darling-launcher +GenericName=Game +Icon=darling-launcher +Keywords=Darling;Launcher;Compatibility; +Name=Darling Launcher StartupWMClass=RiotClientServices Terminal=true Type=Application @@ -264,7 +265,7 @@ DESKTOP_EOF ''; meta = with pkgs.lib; { - description = "Unofficial tooling and patches to run macOS League of Legends on Linux with Darling"; + description = "Unofficial tooling and patches to run the macOS game client on Linux with Darling"; homepage = "https://github.com/lucannez64/darling-lol"; license = licenses.gpl3Only; platforms = [ "x86_64-linux" ]; @@ -334,7 +335,12 @@ DESKTOP_EOF # Tests use private stub binaries/repositories, never Darling itself. python3 -m unittest discover -s tests -v python3 scripts/validate.py - grep -q '^Terminal=true$' ${darlingLol}/share/applications/darling-lol.desktop + grep -q '^Terminal=true$' ${darlingLol}/share/applications/darling-launcher.desktop + 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 -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" ''; }; @@ -349,7 +355,7 @@ DESKTOP_EOF default = { type = "app"; program = "${darlingLol}/bin/darling-lol"; - meta.description = "Launch League of Legends via Darling"; + meta.description = "Launch the macOS game client via Darling"; }; shell = { type = "app"; @@ -359,7 +365,7 @@ DESKTOP_EOF installer = { type = "app"; program = "${darlingLol}/bin/darling-lol-installer"; - meta.description = "Run the Darling and League of Legends rootless installer"; + meta.description = "Run the Darling rootless installer"; }; build = { type = "app"; @@ -371,7 +377,7 @@ DESKTOP_EOF devShells.${system}.default = pkgs.mkShell { packages = buildDeps ++ [ darlingLolFhs ]; shellHook = '' - echo "Darling LoL build & development environment loaded." + echo "Darling build & development environment loaded." echo "Tip: Run 'darling-lol-fhs' to enter the FHS build sandbox if standard paths (/usr/include, /usr/lib) are needed." ''; }; diff --git a/scripts/create-desktop-entry.sh b/scripts/create-desktop-entry.sh index 81b8c22..dbdb7a2 100755 --- a/scripts/create-desktop-entry.sh +++ b/scripts/create-desktop-entry.sh @@ -4,4 +4,4 @@ set -eu repo=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) cd "$repo" python3 scripts/configure.py >/dev/null -python3 build/runtime/tools/riot-desktop-entry.py +python3 build/runtime/tools/desktop-entry.py diff --git a/tools/desktop-entry.py b/tools/desktop-entry.py new file mode 100755 index 0000000..0aa6c76 --- /dev/null +++ b/tools/desktop-entry.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""Create and install the GNOME / FreeDesktop desktop entry and launcher icon.""" +import os +import shutil +import subprocess +from pathlib import Path + +PROJECT_ROOT = Path("@PROJECT_ROOT@") +HOST_HOME = Path("@HOST_HOME@") + + +def remove_legacy_entry(path: Path) -> None: + """Remove a previous launcher entry only when this project created it.""" + try: + if path.exists() and "desktop-run.sh" in path.read_text(errors="ignore"): + path.unlink() + except OSError: + pass + + +def main(): + icons_base = HOST_HOME / ".local/share/icons" + apps_base = HOST_HOME / ".local/share/applications" + + scalable_dir = icons_base / "hicolor/scalable/apps" + png_dir = icons_base / "hicolor/128x128/apps" + scalable_dir.mkdir(parents=True, exist_ok=True) + png_dir.mkdir(parents=True, exist_ok=True) + apps_base.mkdir(parents=True, exist_ok=True) + + source_svg = PROJECT_ROOT / "darling-lol.svg" + source_png = PROJECT_ROOT / "darling-lol.png" + if not source_svg.is_file() or not source_png.is_file(): + raise SystemExit("launcher icon is missing: expected darling-lol.svg and darling-lol.png") + + # 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") + + # Remove artifacts created by older versions of this helper. + remove_legacy_entry(apps_base / "league-of-legends.desktop") + for legacy_icon in ( + icons_base / "league-of-legends.svg", + 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() + 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 + desktop_content = f"""[Desktop Entry] +Version=1.0 +Type=Application +Name=Darling Launcher +GenericName=Game +Comment=Unofficial launcher for the macOS game client via Darling +Exec={exec_cmd} +Path={PROJECT_ROOT} +Icon=darling-launcher +Terminal=false +Categories=Game;ActionGame;StrategyGame; +Keywords=darling;launcher;compatibility; +StartupWMClass=Riot Client +StartupNotify=false +""" + + desktop_file = apps_base / "darling-launcher.desktop" + desktop_file.write_text(desktop_content) + 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" + desktop_shortcut.write_text(desktop_content) + os.chmod(desktop_shortcut, 0o755) + gio = shutil.which("gio") + if gio: + subprocess.run([gio, "set", str(desktop_shortcut), "metadata::trusted", "true"], check=False) + print(f"Created desktop shortcut: {desktop_shortcut}") + + # Refresh icon and desktop databases + update_desktop = shutil.which("update-desktop-database") + if update_desktop: + subprocess.run([update_desktop, str(apps_base)], check=False) + + update_icons = shutil.which("gtk-update-icon-cache") + if update_icons: + subprocess.run([update_icons, "-f", "-t", str(icons_base / "hicolor")], check=False) + + print("GNOME application launcher and icon installed successfully.") + + +if __name__ == "__main__": + main() diff --git a/tools/riot-desktop-entry.py b/tools/riot-desktop-entry.py deleted file mode 100755 index 049e94d..0000000 --- a/tools/riot-desktop-entry.py +++ /dev/null @@ -1,202 +0,0 @@ -#!/usr/bin/env python3 -"""Create and install the GNOME / FreeDesktop desktop entry and launcher icon.""" -import os -import shutil -import subprocess -import sys -from pathlib import Path - -PROJECT_ROOT = Path("@PROJECT_ROOT@") -HOST_HOME = Path("@HOST_HOME@") -DPREFIX = Path("@DPREFIX@") - -SVG_ICON_CONTENT = """ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -""" - - -def extract_best_png_from_icns(icns_path: Path) -> bytes | None: - """Extract the highest-resolution embedded PNG from an Apple ICNS file.""" - try: - data = icns_path.read_bytes() - if not data.startswith(b"icns"): - return None - offset = 8 - best_png = None - best_len = 0 - while offset + 8 <= len(data): - size = int.from_bytes(data[offset + 4 : offset + 8], "big") - if size < 8 or offset + size > len(data): - break - chunk = data[offset + 8 : offset + size] - if chunk.startswith(b"\x89PNG\r\n\x1a\n") and len(chunk) > best_len: - best_png = chunk - best_len = len(chunk) - offset += size - return best_png - except Exception: - return None - - -def find_candidate_icns() -> Path | None: - """Find any available Riot or League .icns icon files.""" - candidates = [ - *list((PROJECT_ROOT / "build/installer").glob("*League of Legends*.app/Contents/Resources/*.icns")), - *list(DPREFIX.glob("Users/Shared/Riot Games/Riot Client.app/Contents/Resources/*.icns")), - *list(DPREFIX.glob("Applications/Riot Client.app/Contents/Resources/*.icns")), - *list(DPREFIX.glob("Applications/League of Legends.app/Contents/Resources/*.icns")), - ] - for c in candidates: - if c.is_file() and c.stat().st_size > 1024: - return c - return None - - -def main(): - icons_base = HOST_HOME / ".local/share/icons" - apps_base = HOST_HOME / ".local/share/applications" - desktop_dir = HOST_HOME / "Desktop" - - scalable_dir = icons_base / "hicolor/scalable/apps" - png_dir = icons_base / "hicolor/512x512/apps" - scalable_dir.mkdir(parents=True, exist_ok=True) - png_dir.mkdir(parents=True, exist_ok=True) - apps_base.mkdir(parents=True, exist_ok=True) - - # 1. Install scalable SVG icon - svg_target = scalable_dir / "league-of-legends.svg" - svg_target.write_text(SVG_ICON_CONTENT) - shutil.copyfile(svg_target, icons_base / "league-of-legends.svg") - - # 2. Extract or generate PNG icon - png_target = png_dir / "league-of-legends.png" - icns_file = find_candidate_icns() - extracted = False - if icns_file: - png_data = extract_best_png_from_icns(icns_file) - if png_data: - png_target.write_bytes(png_data) - shutil.copyfile(png_target, icons_base / "league-of-legends.png") - extracted = True - print(f"Extracted official icon from {icns_file.name} to {png_target}") - - if not extracted and not png_target.exists(): - # Try to rasterize SVG via rsvg-convert or magick - rsvg = shutil.which("rsvg-convert") - magick = shutil.which("magick") or shutil.which("convert") - if rsvg: - subprocess.run([rsvg, "-w", "512", "-h", "512", str(svg_target), "-o", str(png_target)], check=False) - if png_target.exists(): - shutil.copyfile(png_target, icons_base / "league-of-legends.png") - elif magick: - subprocess.run([magick, "-background", "none", "-resize", "512x512", str(svg_target), str(png_target)], check=False) - if png_target.exists(): - shutil.copyfile(png_target, icons_base / "league-of-legends.png") - - # 3. Create .desktop file - 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 - desktop_content = f"""[Desktop Entry] -Version=1.0 -Type=Application -Name=League of Legends -GenericName=MOBA Game -Comment=Play League of Legends on Linux with darling-lol -Exec={exec_cmd} -Path={PROJECT_ROOT} -Icon=league-of-legends -Terminal=false -Categories=Game;ActionGame;StrategyGame; -Keywords=riot;league;lol;darling;games; -StartupWMClass=Riot Client -StartupNotify=false -""" - - desktop_file = apps_base / "league-of-legends.desktop" - desktop_file.write_text(desktop_content) - 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: - desktop_shortcut = ddir / "league-of-legends.desktop" - desktop_shortcut.write_text(desktop_content) - os.chmod(desktop_shortcut, 0o755) - gio = shutil.which("gio") - if gio: - subprocess.run([gio, "set", str(desktop_shortcut), "metadata::trusted", "true"], check=False) - print(f"Created desktop shortcut: {desktop_shortcut}") - - # 4. Refresh icon and desktop databases - update_desktop = shutil.which("update-desktop-database") - if update_desktop: - subprocess.run([update_desktop, str(apps_base)], check=False) - - update_icons = shutil.which("gtk-update-icon-cache") - if update_icons: - subprocess.run([update_icons, "-f", "-t", str(icons_base / "hicolor")], check=False) - - print("GNOME application launcher and icon installed successfully.") - - -if __name__ == "__main__": - main()