An Orbit of Its Own: Yandex Music on System Electron

Translated by a model; the original is in Russian.

· Программы
The story of one package: how the hunt for a window's shadow under Wayland led to rebuilding Yandex Music on top of system Electron, why the finished package ended up three hundred megabytes lighter and updates itself — and why it's happier on GitHub than in the AUR.

cover.svg

A shadow that wasn't there

It all started with a trifle you can't unsee. The official Yandex Music for Linux is an Electron app shipped as a deb package; in the AUR it's neatly repackaged as yandex-music. The app worked, played, showed an icon in the tray — but in a Wayland session its frameless window hung on the screen as a flat rectangle, with no shadow. Every neighboring window cast a shadow, and this one didn't.

The cause turned out to be architectural rather than cosmetic. A window with its own titlebar draws client-side decorations (CSD) and is obliged to draw the shadow itself. Frameless windows in Electron only learned to do this on Wayland as of version 41. And inside the official deb — and, consequently, inside the AUR package — sat a bundled Electron 38. It didn't clear that bar and couldn't have: the runtime version is chosen not by the user but by whoever builds the deb.

Testing it took a single evening: the same application code, launched on the system electron42 from the extra repository, produced a window with a shadow. The diagnosis turned into a plan: take only the application itself from the official deb — the app.asar archive — and run it on the system runtime.

Three snags

The plan looked trivial: unpack the deb, drop app.asar into /opt, write a wrapper electron42 /opt/yandex-music/app.asar. Reality made three amendments, each instructive in its own way.

The tray icon — an empty square. There's space for the icon, the menu works, it's clickable — but there's no image. The application code builds the path to its icons from process.resourcesPath, but when run on system Electron that path points into Electron's own directory (/usr/lib/electron42/resources), where there are, of course, none of Yandex's assets. It was tempting to blame it all on a bug in publishing the icon over D-Bus — the live StatusNotifierItem object honestly showed the menu and status, but not the pixels. In fact this was a consequence, not a cause: when the tray has an empty image, there's simply nothing to put into IconPixmap.

Logged out for no reason. The application name determines the profile directory under ~/.config, where the cookies and session live. Run the bare script and Electron takes the default name, and the app wakes up in a stranger's profile, ~/.config/Electron, having forgotten who you are. The frontal cure — calling app.setName() at runtime — fixed the name but reproducibly broke content loading: an A/B run showed that with setName/setVersion the proprietary music-application:// protocol responds with an error, and without them it works.

The tyranny of semver. Without a package.json next to the entry point, app.getVersion() returns the string "0.0", and the app dies at startup: “App version is not a valid semver version.” What made it especially piquant was that with certain launch methods the version was “accidentally” taken from Electron itself and passed the check — the picture kept shifting.

All three snags are cleared with a single maneuver, and therein lies the build's key insight: /opt/yandex-music is made not a storehouse of files but an application directory — with its own package.json:

{ "name": "YandexMusic", "version": "<версия>", "main": "launcher.js" }

Electron reads this file natively, before any user code: the name gives the correct profile (session intact), the version gives a valid semver (startup passes), and none of the problematic runtime calls. And main points to a thin dozen-line shim that, before loading app.asar, fixes exactly two paths:

'use strict';
const APP_DIR = '/opt/yandex-music';
const ASAR = APP_DIR + '/app.asar';
const { app } = require('electron');

Object.defineProperty(process, 'resourcesPath', { value: APP_DIR, configurable: true });
Object.defineProperty(app, 'getAppPath', { value: () => ASAR, configurable: true });

require(ASAR);

resourcesPath — so the tray icons are found; getAppPath — so the web content inside the asar is found. The process.resourcesPath property is protected against writing, but not against redefinition — configurable: true leaves the door open.

An elegant detail: the version from the feed

The official client updates itself — which means somewhere there's a machine-readable answer to the question “what's the latest version.” It was found: the latest-linux.yml feed on Yandex's servers, listing the version number, the deb file name and its sha512 checksum.

The PKGBUILD is built around this feed. On every rebuild prepare() reads the feed, downloads the current deb and verifies the checksum; pkgver() picks up the version number automatically. Updating the app is a single command:

makepkg -si

No editing the version by hand, no waiting for someone to update a stranger's PKGBUILD. A release comes out — you build it — pacman updates the package as usual. The build is always “latest” by definition, because the source of truth is the very same feed the official auto-updater uses.

The side effect of moving to system Electron is pleasant in its own right: the bundled copy of Chromium vanished from the package — minus three hundred megabytes of duplicated code that, on top of that, got no security updates between Music releases.

Cutting ties with the predecessor

As long as the package was named yandex-music, AUR helpers considered it their own: they saw a fresher package of the same name in the AUR and obligingly offered an “update” that would have wiped out the whole build with bundled Electron 38 — along with the very shadow the whole thing was started for.

This is cured at the level of the package itself, not the machine's settings: the package was renamed to yandex-music-system-electron, and the lines

provides=('yandex-music')
conflicts=('yandex-music')

ensure continuity: an installed predecessor is replaced in a single pacman transaction, dependencies on the old name keep being satisfied, but foreign updates no longer arrive — there's no package by that name in the AUR.

Why not in the AUR

You'd think the natural finale would be to publish the result in the AUR alongside the predecessor: the “on system Electron” variant differs substantively, and such packages are accepted there.

But the AUR has a charter, and that charter was written for a different design. Sources must reside in the source=() array with checksums stitched into the text of the PKGBUILD; a dynamic pkgver() is permitted only for VCS packages. In other words — no feed: the version and checksum have to be typed in by hand for every release, and Music's releases are frequent. The very scheme the package was built for — self-updating, with integrity checked over the same channel the official client trusts — simply doesn't fit into that frame.

One could go argue on the mailing lists that the rules ought to be read more broadly. But the charter is older than the task, its keepers are unyielding, and the outcome of such disputes is known in advance. Rather than amputating the best part of the solution for the sake of a line in someone else's catalog, the package lives on GitHub: git clone, makepkg -si — and you always have fresh Music on system Electron. Even the distribution needn't be the same: the approach carries over to any Electron app you'd like to pull off its bundled runtime and set on the system one.

The bottom line

  • A clean package, tracked by pacman, ~300 MB lighter than the official one.
  • Native Wayland — and that very window shadow.
  • Tray icon, MPRIS, stream playback through ALSA at up to 176.4 kHz.
  • Updating with a single command, version and checksum straight from the official feed.
  • Full independence from the same-named AUR package.

The window shadow is a trifle. But it's precisely the trifles that make you take the system apart down to the last screw, and what's put back together turns out better than the original: lighter, fresher, and in an orbit of its own.

Comments

Sign in, to comment.

  • No comments yet.