diff --git a/.github/workflows/publish-winget.yml b/.github/workflows/publish-winget.yml index 2657874ba..13f109753 100644 --- a/.github/workflows/publish-winget.yml +++ b/.github/workflows/publish-winget.yml @@ -13,13 +13,41 @@ on: jobs: publish: runs-on: windows-latest - if: (github.event_name == 'workflow_dispatch' || !github.event.release.prerelease) && vars.WINGET_IDENTIFIER != '' + # La condition de configuration a QUITTÉ ce `if`, et c'est tout l'objet du + # changement. `vars.WINGET_IDENTIFIER != ''` ici rendait le job `skipped`, et un + # job sauté est vert : les huit dernières releases, v1.8.0 comprise, ont toutes + # « réussi » sans rien publier, et #148 est resté ouvert deux semaines parce que + # rien, nulle part, ne le disait. Même famille que le glob zsync mort de + # build.yml — un garde qui ne fait rien en silence ne garde rien. + # + # Le job démarre donc toujours, et c'est une étape qui annonce l'absence de + # configuration. Le coût est une minute de runner par release ; le gain est que + # « pas publié » devienne visible dans le résumé du run. + if: github.event_name == 'workflow_dispatch' || !github.event.release.prerelease + env: + IDENTIFIER: ${{ vars.WINGET_IDENTIFIER }} + # `secrets` n'est PAS un contexte lisible depuis un `if`, ni au niveau du job ni + # au niveau de l'étape — seul `env` l'est. D'où ce booléen-en-chaîne, qui expose + # la présence du token sans jamais exposer sa valeur. + HAS_TOKEN: ${{ secrets.WINGET_ACC_TOKEN != '' }} steps: + # Les deux pièges nommés ici sont documentés chez l'action et coûteraient chacun + # une release pour être découverts : un PAT fine-grained N'EST PAS supporté + # (vedantmgoyal9/winget-releaser#172), et l'action refuse de tourner tant qu'aucune + # version du paquet n'existe déjà dans winget-pkgs — elle part d'un manifeste + # existant pour écrire le suivant. La première soumission est donc manuelle, + # `wingetcreate new`, et l'automatisation ne prend le relais qu'ensuite. + - name: Report that winget publishing is not configured + if: env.IDENTIFIER == '' || env.HAS_TOKEN != 'true' + run: | + echo "::warning title=winget publishing skipped::Nothing was published to winget. Needs (1) the repository variable WINGET_IDENTIFIER, currently ${{ env.IDENTIFIER == '' && 'UNSET' || 'set' }}; (2) the secret WINGET_ACC_TOKEN, currently ${{ env.HAS_TOKEN == 'true' && 'set' || 'UNSET' }} — a CLASSIC PAT with public_repo scope, fine-grained tokens are not supported; (3) a fork of microsoft/winget-pkgs under getopenscreen; and (4) at least one version of the package already merged into winget-pkgs, since this action updates an existing manifest and refuses to create the first one. See https://github.com/getopenscreen/openscreen/issues/148" + # Épinglé sur le SHA de v2 : un tag git est mutable, et cette action tierce # reçoit WINGET_ACC_TOKEN. Un tag repointé suffirait à exfiltrer le token # sans qu'aucun changement n'apparaisse ici. Pour bouger de version, # re-résoudre le tag et remplacer le SHA explicitement. - uses: vedantmgoyal9/winget-releaser@4ffc7888bffd451b357355dc214d43bb9f23917e # v2 + if: env.IDENTIFIER != '' && env.HAS_TOKEN == 'true' with: identifier: ${{ vars.WINGET_IDENTIFIER }} # Matches the Windows installer asset attached to each release, diff --git a/electron-builder.json5 b/electron-builder.json5 index 35dde88ad..86dbad7c2 100644 --- a/electron-builder.json5 +++ b/electron-builder.json5 @@ -142,15 +142,17 @@ // Mesa est ce qui garantit qu'au minimum le rastériseur logiciel existe. // // `depends` REMPLACE la liste par défaut d'electron-builder au lieu de s'y ajouter - // (app-builder-lib, FpmTarget.getDefaultDepends) : les NEUF premières entrées sont - // donc ce défaut, verbatim, suivies des quatre qui nous sont propres. En retirer une - // casse le paquet silencieusement. + // (app-builder-lib, FpmTarget.getDefaultDepends) : le défaut y est repris verbatim, + // de `libgtk-3-0` à `libsecret-1-0`, et en retirer une casse le paquet silencieusement. + // Ce qui suit est à NOUS, et se lit par nom plutôt que par position — un compteur + // « les N dernières » se périme à chaque ajout, et l'a déjà fait : + // `mesa-vulkan-drivers`, `libgbm1`, `libasound2`, `libgomp1`, `xdg-desktop-portal`. // // L'AppImage n'a pas de mécanisme de dépendances et reste donc exposée : c'est // pour elle que `d3d_linux::diagnose` nomme le paquet à installer. // - // Les trois dernières comblent des sonames que rien ne déclarait ni n'embarquait, - // mesurés sur la 1.9.1 installée dans une Ubuntu 22.04 nue : + // `libgbm1`, `libasound2` et `libgomp1` comblent des sonames que rien ne déclarait ni + // n'embarquait, mesurés sur la 1.9.1 installée dans une Ubuntu 22.04 nue : // // libgbm1 — réclamé par le binaire Electron LUI-MÊME. Sans lui l'app sort en // 127 sur `libgbm.so.1: cannot open shared object file`, avant toute @@ -168,6 +170,27 @@ // libs média périphériques. Une install minimale ou un WM monté à la main n'a // aucune de ces garanties. C'est ce que le job `verify-linux-package` prouve // désormais à chaque build, en interrogeant le loader plutôt que cette liste. + // + // `xdg-desktop-portal` est la MÊME classe de bug, et le job ci-dessus ne la verra + // jamais : il interroge ld.so, or un portail est un service D-Bus et n'apparaît dans + // aucun DT_NEEDED. Toute la capture Linux passe par lui — X11 compris, le helper + // n'a pas d'autre chemin — et sans lui l'app affiche l'erreur zbus brute (#324). + // + // Le déclarer ne suffit PAS à lui seul, et il faut le dire clairement plutôt que + // laisser croire que la ligne referme le sujet : le frontend ne fait que router, et + // l'implémentation de ScreenCast vient d'un backend qui dépend du bureau + // (-gnome, -kde, -hyprland, -wlr, -gtk). On ne peut pas en choisir un ici sans se + // tromper sur la moitié des machines. Ce que la déclaration achète, c'est que la + // contrainte devient explicite au lieu d'être héritée par accident du métapaquet de + // bureau, exactement comme les trois sonames au-dessus. Le reste est fait à + // l'exécution par `portal.rs::portal_unavailable`, qui nomme le backend à installer — + // et c'est la seule moitié qui couvre l'AppImage, qui ne déclare rien. + // + // Nom identique sur les trois distributions, et vérifié plutôt que supposé : + // `verify-linux-package.sh` installe réellement le paquet dans une image nue, où un nom + // inexistant fait échouer la transaction entière — c'est comme ça que `http-parser` a + // été pris sur pacman. Les trois formats répondent `ok install: declared depends + // resolved` sur ubuntu:22.04, fedora:40 et archlinux:latest (run 31391020354). "deb": { "depends": [ "libgtk-3-0", @@ -182,15 +205,17 @@ "mesa-vulkan-drivers", "libgbm1", "libasound2", - "libgomp1" + "libgomp1", + "xdg-desktop-portal" ] }, "pacman": { // `vulkan-swrast` est le lavapipe d'Arch ; il tire `vulkan-icd-loader` avec lui. // - // Les trois derniers sont les équivalents Arch des sonames décrits sur `deb`. - // Noms relevés dans un conteneur archlinux, pas devinés : `libgomp.so.1` - // appartient à `libgomp` (core), PAS à `gcc-libs` — il en a été sorti. + // `mesa`, `alsa-lib` et `libgomp` sont les équivalents Arch des sonames décrits sur + // `deb`, et `xdg-desktop-portal` y porte le même nom. Noms relevés dans un conteneur + // archlinux, pas devinés : `libgomp.so.1` appartient à `libgomp` (core), PAS à + // `gcc-libs` — il en a été sorti. // // `http-parser` A ÉTÉ RETIRÉ, et c'est la seule entrée de ce fichier qui s'écarte // volontairement du défaut d'electron-builder. Arch l'a supprimé de ses dépôts @@ -218,13 +243,16 @@ "vulkan-swrast", "mesa", "alsa-lib", - "libgomp" + "libgomp", + "xdg-desktop-portal" ] }, "rpm": { - // Même règle que ci-dessus : la liste REMPLACE le défaut d'electron-builder, - // donc les huit premières entrées SONT ce défaut (FpmTarget.getDefaultDepends, - // cas "rpm"), verbatim, suivies des deux qui nous sont propres. + // Même règle que ci-dessus : la liste REMPLACE le défaut d'electron-builder, donc + // celui-ci est repris verbatim de `gtk3` à `(libuuid or libuuid1)` + // (FpmTarget.getDefaultDepends, cas "rpm"). Le reste est à nous, par nom et non par + // position : `libsecret`, `mesa-vulkan-drivers`, `mesa-libgbm`, `alsa-lib`, + // `libgomp`, `xdg-desktop-portal`. // // `mesa-vulkan-drivers` porte le même nom sur Fedora que sur Debian : c'est le // paquet qui garantit lavapipe, sans quoi l'aperçu du compositeur est indisponible. @@ -235,11 +263,12 @@ // sans lui, `isEncryptionAvailable()` répond faux et l'enregistrement d'une clé // lève. Une omission d'electron-builder, pas un choix. // - // Les trois derniers sont les équivalents Fedora des sonames décrits sur `deb`, - // relevés par `dnf provides` : `libgbm.so.1` vient de `mesa-libgbm` et non du - // `mesa` d'Arch. `libgomp` fait partie de l'install de base sur Fedora, donc le - // rpm y était moins exposé que le deb — le déclarer reste ce qui rend la - // contrainte vraie plutôt que chanceuse. + // `mesa-libgbm`, `alsa-lib` et `libgomp` sont les équivalents Fedora des sonames + // décrits sur `deb`, relevés par `dnf provides` : `libgbm.so.1` vient de + // `mesa-libgbm` et non du `mesa` d'Arch. `libgomp` fait partie de l'install de base + // sur Fedora, donc le rpm y était moins exposé que le deb — le déclarer reste ce qui + // rend la contrainte vraie plutôt que chanceuse. `xdg-desktop-portal` y porte le même + // nom que sur Debian et Arch. "depends": [ "gtk3", "libnotify", @@ -253,7 +282,8 @@ "mesa-vulkan-drivers", "mesa-libgbm", "alsa-lib", - "libgomp" + "libgomp", + "xdg-desktop-portal" ] }, "win": { diff --git a/electron/native/pipewire-capture/src/portal.rs b/electron/native/pipewire-capture/src/portal.rs index 33ed9f1f0..1d23e7670 100644 --- a/electron/native/pipewire-capture/src/portal.rs +++ b/electron/native/pipewire-capture/src/portal.rs @@ -138,14 +138,39 @@ fn failed(context: &str, error: impl std::fmt::Display) -> PortalError { PortalError::Failed(format!("{context}: {error}")) } +/// `Screencast::new()` fails when nothing answers on the ScreenCast D-Bus +/// interface. That is almost never a fault in the session: it is a machine with +/// no portal BACKEND installed — invisible on any desktop that ships one, which +/// every distro's desktop metapackage does, and the norm on a minimal install or +/// a hand-assembled compositor. +/// +/// This message is not a log line. `message()` is what the helper emits on +/// `portal-failed`, what `handlers.ts` turns into the IPC error, and what the +/// recorder finally shows in a toast — so the raw zbus text ("A portal frontend +/// implementing org.freedesktop.portal.ScreenCast was not found") reached the +/// user verbatim and read as an internal fault to the one person able to fix it. +/// Name the package instead, the way `d3d_linux::diagnose` names Mesa. +/// +/// The list has to be a list. `xdg-desktop-portal` on its own only dispatches; +/// which backend implements ScreenCast depends on the desktop, so naming the +/// frontend alone would send someone to install the package they already have. +fn portal_unavailable(error: impl std::fmt::Display) -> PortalError { + PortalError::Failed(format!( + "no ScreenCast portal is available on this session ({error}). Screen capture on Linux \ + goes through xdg-desktop-portal, which needs the backend matching your desktop: \ + xdg-desktop-portal-gnome (GNOME), xdg-desktop-portal-kde (KDE Plasma), \ + xdg-desktop-portal-hyprland (Hyprland), xdg-desktop-portal-wlr (Sway and other wlroots \ + compositors), or xdg-desktop-portal-gtk (anything else). Install one, then log out and \ + back in." + )) +} + /// Cheap, non-interactive probe: does this portal offer METADATA cursor mode? /// /// Split out from [`negotiate`] so the helper can answer that question — and /// emit `ready` — before anything raises a dialog. pub async fn cursor_metadata_supported() -> Result { - let proxy = Screencast::new() - .await - .map_err(|error| failed("cannot reach org.freedesktop.portal.ScreenCast", error))?; + let proxy = Screencast::new().await.map_err(portal_unavailable)?; let cursor_modes = proxy .available_cursor_modes() .await @@ -161,9 +186,7 @@ pub async fn cursor_metadata_supported() -> Result { /// process-global `OnceLock`, so the portal session stays open until this /// process exits — which is exactly the lifetime we want. pub async fn negotiate(cursor_mode: CursorMode) -> Result { - let proxy = Screencast::new() - .await - .map_err(|error| failed("cannot reach org.freedesktop.portal.ScreenCast", error))?; + let proxy = Screencast::new().await.map_err(portal_unavailable)?; let cursor_modes = proxy .available_cursor_modes() diff --git a/technical-documentation/engineering/build-and-packaging.md b/technical-documentation/engineering/build-and-packaging.md index d7fbdd3c4..cc1b39c15 100644 --- a/technical-documentation/engineering/build-and-packaging.md +++ b/technical-documentation/engineering/build-and-packaging.md @@ -135,6 +135,8 @@ The exposure is instead handled at the source, which is the layer to prefer anyw What remains host-supplied for the AppImage is the GTK/GLib/NSS stack, which no AppImage bundles — theme engines, GIO modules and pixbuf loaders all resolve against the host. `libvulkan.so.1` is already bundled at the AppImage root by electron-builder itself. For the Vulkan *driver*, which cannot be bundled, `d3d_linux::diagnose` names the Mesa package instead. +**One dependency class stays invisible to everything above**, and 1.9.3 adds it after #328: `xdg-desktop-portal`. All Linux capture goes through it — X11 included, the helper has no other path — but a portal is a D-Bus service, so it appears in no `DT_NEEDED` entry and `ldd` will never name it however bare the container is. Note precisely which half the check still holds: it cannot tell you the portal is *missing* from the list, but the install step does prove that a name you put there exists, which is what confirmed `xdg-desktop-portal` on all three distros. It hid behind the same metapackage accident as the three sonames, and it is now declared on `deb`, `rpm` and `pacman`. Declaring it is only half the fix: the frontend merely routes, and the ScreenCast implementation comes from a desktop-specific backend (`-gnome`, `-kde`, `-hyprland`, `-wlr`, `-gtk`) that no `depends` list here can choose without being wrong on half the machines. The other half is `portal.rs::portal_unavailable`, which names those backends in the error the user actually sees — the same answer `diagnose` gives for the Vulkan driver, and the only one that reaches the AppImage. + ### Testing without the build machine's advantages Every failure in this section shares one shape: **the machines we test on have more installed than the machines we ship to.** A developer box carries the Visual C++ Redistributable because Visual Studio put it there; a CI runner carries a newer glibc than the distros the README claims. Nothing run on either can reveal an absence, so "it works here" is not evidence about anything, however many times it is repeated.