diff --git a/.github/workflows/desktop-fedora.yml b/.github/workflows/desktop-fedora.yml
new file mode 100644
index 00000000..e06883fe
--- /dev/null
+++ b/.github/workflows/desktop-fedora.yml
@@ -0,0 +1,16 @@
+name: Standalone desktop (Fedora)
+on:
+ push:
+ pull_request:
+jobs:
+ desktop:
+ runs-on: ubuntu-latest
+ container: registry.fedoraproject.org/fedora:44
+ steps:
+ - name: Build dependencies
+ run: dnf install -y git gcc make binutils python3 pkgconf-pkg-config wayland-devel wayland-protocols-devel libxkbcommon-devel libxcrypt-devel libseat-devel openssl-devel nodejs npm
+ - uses: actions/checkout@v4
+ - run: npm ci
+ - run: make -j2 check CFLAGS='-O2 -g -D_FORTIFY_SOURCE=3 -fstack-protector-strong'
+ - name: Stage installation
+ run: make install PREFIX=/usr DESTDIR="$RUNNER_TEMP/lord-de"
diff --git a/.gitignore b/.gitignore
index 6439c107..7be14615 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,6 +18,9 @@ assets/locales/*.tgz
logs/
/code-manifest.json
/code-manifest.sig
+/build-profile.json
+*.rpm
+/dist/
# the desktop imager's builds (desktop/imager, docs/IMAGER.md)
desktop/imager/target/
desktop/imager/dist/
diff --git a/Makefile b/Makefile
new file mode 100644
index 00000000..5ae5af67
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,17 @@
+PREFIX ?= /usr/local
+LIBEXECDIR ?= $(PREFIX)/libexec
+PYTHON ?= python3
+
+.PHONY: all themes server check test install clean
+all: themes server
+themes:
+ LORD_PROFILE=desktop npm run build
+server:
+ $(MAKE) -C server PROFILE=desktop
+check test: all
+ $(MAKE) -C server PROFILE=desktop test-platform
+ $(PYTHON) tools/desktop/check.py
+install:
+ $(PYTHON) tools/desktop/install.py --prefix '$(PREFIX)' --libexecdir '$(LIBEXECDIR)' --destdir '$(DESTDIR)'
+clean:
+ $(MAKE) -C server clean
diff --git a/README.md b/README.md
index 5b080253..d0ed3214 100644
--- a/README.md
+++ b/README.md
@@ -1,127 +1,86 @@
-
-
-
-
-
-
-
-The free, fast and secure Linux desktop.
-
-
- lordos.org ·
- Download ·
- Features ·
- Gallery ·
- Paper ·
- FAQ ·
- Docs
-
-
-LordOS is a free GNU/Linux distribution based on Debian, with its own desktop environment:
-a display server and Wayland compositor written in C, its own interface engine, and native apps
-made to feel calm, clear and private. It runs on 64-bit PCs and on the Raspberry Pi 5, and the
-Linux apps you already use (Chromium, Firefox, GTK and Qt apps) run alongside the native ones.
-
-
-
-
-
-
-## Everything you need. Nothing you don't.
-
-| | |
-|---|---|
-| **[A desktop of its own](https://lordos.org/features/own-desktop/)** | Its own display server and Wayland compositor in C and its own interface engine, on a solid Debian base. Smooth on any 64-bit PC (amd64), and light enough for the Raspberry Pi 5 (arm64). |
-| **[Private by design](https://lordos.org/features/private-by-design/)** | Installable LordOS apps run isolated and ask before using your camera, microphone, files or network. You decide. |
-| **[A firewall you can see](https://lordos.org/features/visible-firewall/)** | Watch every connection on a live world map, then allow or block it per app, by domain, address or network, and restrict camera, microphone, files and more. |
-| **[Passwords built in](https://lordos.org/features/passwords/)** | A vault for passwords, 2FA codes and cards that unlocks with a password or a USB key. |
-| **[Mail, Calendar, Contacts](https://lordos.org/features/mail-calendar/)** | Native apps for your accounts, with remote images blocked until you say so. |
-| **[Your Linux apps too](https://lordos.org/features/linux-apps/)** | Chromium, Firefox and GTK apps fit right in, with the same window frame and dark mode. |
-
-Measured, not promised: the whole system idles at **465–487 MB of RAM**, of which `lord-server`
-(the display server with the whole desktop) is **32–36 MB**, and boots in about **16 s** (see
-[`docs/ISO.md`](docs/ISO.md), "Final ISO test"). What is isolated today, exactly, is in
-[`docs/PRIVACY.md`](docs/PRIVACY.md).
-
-## Get LordOS
-
-Releases are signed and published on [GitHub Releases](https://github.com/lordbasex/LordOS/releases)
-and on [lordos.org](https://lordos.org/#download).
-
-- **PC (64-bit):** a live and installable ISO. Try it without installing.
-- **Raspberry Pi 5:** a ready-to-flash image for SD card or NVMe, with fan control and our own
- kernel. The Raspberry Pi 5 is the only supported board.
-- **Already on Debian 13?** Install LordOS on top of your current system with one command:
-
- ```sh
- curl -fsSL https://lordos.org/install.sh | sh
- ```
-
-**Verify your download.** `SHA256SUMS` is signed with the LordOS release key (Ed25519),
-fingerprint `c7155b8e6bbbb180f907381994d06af76d333016cd6f3facf67cadd053ea108e`. See
-[code signing](https://lordos.org/legal/code-signing/) and [`docs/SIGNING.md`](docs/SIGNING.md).
-
-Status: **beta**. See [`CHANGELOG.md`](CHANGELOG.md) and [`docs/ROADMAP.md`](docs/ROADMAP.md).
-
-## Documentation
-
-- [`docs/README.md`](docs/README.md): the index of every document.
-- [The paper](https://lordos.org/paper/) ([`docs/PAPER.md`](docs/PAPER.md)): vision, architecture,
- security model, performance and roadmap, with links to the source files.
-- [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md): components, processes and dependencies.
-
-## Repository map
-
-- `server/`: `lord-server` (display server and Wayland compositor: DRM/KMS and evdev straight
- from the kernel, without X11, libdrm or libinput) plus `lord-sessiond`, `lord-netd`,
- `lord-netperm`, `lord-fand` and the sandboxed helpers (`lord-media`, `lord-doc`, `lord-vault`,
- `lord-maild`).
-- `runtime/`: `@lordos/runtime`, the React renderer for lord-server.
-- `sdk/`: `@lordos/sdk`, the API for themes and apps.
-- `apps/`: the native apps. See [`apps/README.md`](apps/README.md).
-- `themes/`: complete desktops in React, swappable live. See [`themes/README.md`](themes/README.md).
-- `assets/`: fonts, icons, the brand and Lumo, and the UI texts per language.
-- `tools/`: ISO and Raspberry Pi image builders, the Debian installer, release signing, the website.
-- `web/`: the [lordos.org](https://lordos.org) website.
-- `third_party/`: QuickJS, stb, Monocypher and word lists, vendored (see [`THIRD_PARTY.md`](THIRD_PARTY.md)).
-
-## Building from source
-
-On the development machine (needs Node.js):
+# Lord DE
+
+Fork do [LordOS](https://github.com/lordbasex/LordOS) dedicado a extrair e desenvolver seu ambiente de desktop como um projeto independente da distribuição. Fedora é a primeira plataforma de integração deste fork.
+
+O compositor Wayland em C, o motor gráfico, o runtime React, os temas e os aplicativos nativos continuam sendo os componentes do desktop. A distribuição fornece o kernel, o login, a rede, o áudio e o gerenciamento dos pacotes do sistema.
+
+**Estado: experimental.** A compilação e a inicialização sem monitor são verificáveis em contêiner Fedora. A sessão física com logind/seatd ainda precisa ser validada em hardware, incluindo troca de TTY, GPU, suspensão e SELinux.
+
+## Compilar no Fedora
+
+```sh
+sudo dnf install gcc make binutils python3 pkgconf-pkg-config \
+ wayland-devel wayland-protocols-devel libxkbcommon-devel \
+ libxcrypt-devel libseat-devel openssl-devel nodejs npm
+npm ci
+make -j4
+make check
+```
+
+Node.js e npm são usados apenas para compilar os temas. A execução do desktop não depende deles. O caminho padrão usa a composição por CPU quando as bibliotecas Mesa de desenvolvimento estão ausentes. A composição por GPU continua opcional:
+
+```sh
+sudo dnf install mesa-libgbm-devel mesa-libEGL-devel mesa-libGLES-devel
+make -j4
+```
+
+Documentação dos pacotes: [Wayland](https://packages.fedoraproject.org/pkgs/wayland/wayland-devel/), [libxkbcommon](https://packages.fedoraproject.org/pkgs/libxkbcommon/libxkbcommon/), [libseat](https://packages.fedoraproject.org/pkgs/seatd/libseat-devel/), [Node.js](https://developer.fedoraproject.org/tech/languages/nodejs/nodejs.html).
+
+## Instalar e executar
+
+Para inspecionar a instalação sem alterar o sistema:
+
+```sh
+make install PREFIX=/usr DESTDIR="$PWD/build/stage"
+```
+
+Para uma instalação local com entrada no gerenciador de login:
```sh
-npm install && npm run build # bundles every theme with the SDK and the apps
+sudo make install PREFIX=/usr
```
-On Debian 13 (see [`docs/VM-SETUP.md`](docs/VM-SETUP.md) for the full list of packages):
+A instalação adiciona `lord-de-session`, os arquivos em `/usr/libexec/lord-de` e a sessão **Lord DE (experimental)**. Não habilita serviços nem substitui o gerenciador de login. Encerre a sessão atual e selecione essa entrada no login, ou execute `lord-de-session` em uma TTY local livre. Execute o desktop como seu usuário normal, **sem sudo**. O acesso a DRM e evdev passa por libseat com logind ou seatd.
+
+Para verificar a inicialização sem monitor, dentro do checkout:
+
+```sh
+cd server
+./build/lord-server --headless 800x600 --timeout 2
+```
+
+Esse modo não abre uma janela dentro do GNOME/KDE. `Ctrl+Alt+Backspace` encerra a sessão física; `Ctrl+Alt+F1` a `F10` solicita a troca de TTY ao provedor de sessão.
+
+## RPM local
```sh
-make -C server -j4 && make -C server test
+sudo dnf install rpm-build
+sh tools/desktop/dist.sh
+mkdir -p build/rpmbuild/SOURCES
+cp dist/lord-de-*.tar.gz build/rpmbuild/SOURCES/
+rpmbuild -ba --define "_topdir $PWD/build/rpmbuild" packaging/rpm/lord-de.spec
```
-Images: [`docs/ISO.md`](docs/ISO.md) (PC) and [`docs/RASPBERRY.md`](docs/RASPBERRY.md) (Raspberry Pi 5).
+O arquivo fonte inclui os temas compilados a partir do lockfile; o build do RPM compila o C sem acessar a rede. É um pacote local experimental, ainda sem preparação para submissão aos repositórios oficiais do Fedora. O RPM usa a composição por CPU para manter suas dependências pequenas.
-## Contributing and security
+## Separação entre desktop e SO
-- [`CONTRIBUTING.md`](CONTRIBUTING.md) and the [code of conduct](CODE_OF_CONDUCT.md).
-- Found a vulnerability? Please read [`SECURITY.md`](SECURITY.md) and don't open a public issue.
-- Bugs and ideas: [how to report](https://lordos.org/legal/issues/).
+O perfil padrão `desktop` não compila o gerenciador Debian, o instalador do SO, o helper administrativo privilegiado, `lord-netd`, `lord-sessiond`, `lord-netperm` ou `lord-fand`. A loja administra aplicativos `.lpk` e oferece abrir o gerenciador de software da distribuição quando disponível. Pacotes RPM/DNF não são tratados como pacotes Debian.
-## Support the project
+Rede e VPN, usuários, SSH, ventoinhas, formatação de discos e outras operações administrativas do antigo helper ficam com o sistema hospedeiro. As páginas que ainda dependem dele têm funcionalidades limitadas. Áudio usa o protocolo PulseAudio, incluindo PipeWire; Bluetooth e outras integrações existentes continuam usando D-Bus.
-LordOS is free and open source. If it makes your computer better, help us keep building it:
-[lordos.org/#support](https://lordos.org/#support).
+O código e os scripts de distribuição do upstream permanecem no repositório como legado, fora da instalação da DE. Para trabalhar explicitamente no perfil antigo: `make -C server PROFILE=os` e `LORD_PROFILE=os npm run build`. Esse perfil continua específico do LordOS/Debian e não foi convertido em uma distribuição Fedora.
-## Made by iPERFEX
+Veja [a arquitetura da extração e os próximos passos](docs/STANDALONE.md). A [documentação original](docs/README.md) descreve o LordOS upstream; seus comandos de ISO, instalação e administração não se aplicam automaticamente a este fork.
-LordOS is developed by [iPERFEX](https://www.iperfex.com/?lang=en). Meet Lumo, the guardian of
-LordOS, in the [brand guide](docs/LORDOS-BRAND.md). To work with us on LordOS (sponsorship,
-collaborations, custom apps or support), see [lordos.org/about](https://lordos.org/about/).
+## Estrutura
-## License
+- `server/`: compositor, motor gráfico, helpers de aplicativos e camada `src/platform/`.
+- `runtime/`, `sdk/`, `themes/`, `apps/`: interface e aplicativos nativos.
+- `tools/desktop/`: instalação, verificação e arquivo fonte para empacotamento.
+- `packaging/desktop/`, `packaging/rpm/`: integração da sessão e pacote Fedora.
+- `tools/iso/`, `tools/rpi/`, `packaging/debian/`: legado da distribuição original.
-MIT for LordOS's code (see [`LICENSE`](LICENSE)). Each third-party component keeps its own
-license (see [`THIRD_PARTY.md`](THIRD_PARTY.md) and [`docs/COMPLIANCE.md`](docs/COMPLIANCE.md)).
-The LordOS name, logo and Lumo are covered by the [trademark policy](https://lordos.org/legal/trademark/).
+## Origem e licenças
-Copyright © 2026 [iPERFEX](https://www.iperfex.com/?lang=en). All rights reserved for the website, brand and content; the code is free software under the MIT license.
+O código deriva do LordOS, desenvolvido pela iPERFEX. Créditos e avisos de copyright originais são preservados. Consulte [LICENSE](LICENSE) e [THIRD_PARTY.md](THIRD_PARTY.md) para o código e seus componentes. Este fork não é uma versão oficial do LordOS.
diff --git a/apps/settings/General.jsx b/apps/settings/General.jsx
index f2fe8ee3..33b4aa7d 100644
--- a/apps/settings/General.jsx
+++ b/apps/settings/General.jsx
@@ -1,7 +1,7 @@
// Settings → General (machine and software details) and Updates (Debian and LordOS).
import {
View, Text, Button, useTheme, useLocale, useSystemInfo, useServerStats, lordosVersion, screen, formatBytes, formatDuration, formatNumber,
- admin, wm, LordOSLogo, LordOSWordmark, spacing, typeScale,
+ admin, wm, LordOSLogo, LordOSWordmark, spacing, typeScale, standaloneDesktop,
} from '@lordos/sdk';
import { useState, version as reactVersion } from 'react';
import { Stack, Section, ValueRow, Note, Message, Actions } from './ui.jsx';
@@ -58,6 +58,12 @@ export function Updates() {
const pkg = typeof lord.pkg !== 'undefined' && lord.pkg.available();
const ups = pkg ? lord.pkg.upgrades() : [];
const at = pkg ? lord.pkg.lastUpdate() : 0;
+ if (standaloneDesktop) return (
+
+ {tr('store.host.description')}
+ wm.open('store', { args: { page: 'updates' } })} />
+
+ );
const check = () => {
setBusy(true);
admin.run('pkg-update').then(() => setMsg({ ok: true, text: tr('settings.updates.checked') }), (e) => setMsg({ ok: false, text: e.message }))
@@ -74,7 +80,7 @@ export function Updates() {
wm.open('store', { args: { page: 'updates' } })} />
- {!admin.available() && {tr('settings.admin.unavailable')} }
+ {!admin.available() && {tr(standaloneDesktop ? 'settings.host.adminUnavailable' : 'settings.admin.unavailable')} }
);
}
diff --git a/apps/settings/Sharing.jsx b/apps/settings/Sharing.jsx
index 0d37b3ca..0784d3ac 100644
--- a/apps/settings/Sharing.jsx
+++ b/apps/settings/Sharing.jsx
@@ -1,3 +1,4 @@
+import { standaloneDesktop } from '@lordos/sdk';
// Settings → Sharing: remote desktop (lord-sessiond, docs/REMOTE.md) and remote login (SSH).
// Both are off by default; turning them on, choosing who can log in and viewing or cutting
// connections goes through the administration helper (remote-* and ssh-* actions). The SSH
@@ -300,7 +301,7 @@ export function SharingSettings() {
options={[['all', tr('settings.sharing.allUsers')], ['group', tr('settings.sharing.onlyGroup')]]} />
- {!isAdmin && {tr('settings.admin.unavailable')} }
+ {!isAdmin && {tr(standaloneDesktop ? 'settings.host.adminUnavailable' : 'settings.admin.unavailable')} }
{!st.installed && {tr('settings.sharing.notInstalled')} }
diff --git a/apps/settings/index.jsx b/apps/settings/index.jsx
index c165ed85..44bbbe44 100644
--- a/apps/settings/index.jsx
+++ b/apps/settings/index.jsx
@@ -3,7 +3,7 @@
// keyboard, themes and session pages. The other pages are in their own files; all of them are built
// from ./ui.jsx (docs/DESIGN.md, "Settings").
import {
- View, Text, Image, useTheme, useSettings, themes, session, keyboard, admin, languages, setLanguage, useLocale,
+ standaloneDesktop, View, Text, Image, useTheme, useSettings, themes, session, keyboard, admin, languages, setLanguage, useLocale,
reloadCatalogs, tOptional, statusText, fs, useStore, SidebarItem, Toggle, Button, TextInput, ScrollView, SearchField, Hairline, useSystemInfo, useAppWindow, useTrackpads,
spacing, typeScale, registerDockActions, hasTouchpad, findWallpaper, wallpaperThumb, WALLPAPER_CATEGORIES, wm,
} from '@lordos/sdk';
@@ -349,7 +349,7 @@ function LanguageRegion() {
{ reloadCatalogs(); setMsg({ ok: true, text }); }} />
- {!isAdmin && {tr('settings.admin.unavailable')} }
+ {!isAdmin && {tr(standaloneDesktop ? 'settings.host.adminUnavailable' : 'settings.admin.unavailable')} }
@@ -528,7 +528,7 @@ const GROUPS = [
];
const SECTIONS = GROUPS.flat();
// Trackpad only if there is a touchpad (lord-sessiond lends them with the monitor)
-const sectionShown = (id, hasPad) => id !== 'trackpad' || hasPad;
+const sectionShown = (id, hasPad) => (id !== 'trackpad' || hasPad) && (!standaloneDesktop || !['sharing', 'users', 'cooling'].includes(id));
// The Dock's right-click menu lists every section, in the sidebar's order and with its names; one
// opens Settings on that page (sdk/dock.js, quick actions). Built from SECTIONS, so it never drifts.
@@ -575,7 +575,10 @@ export default function Settings() {
const { args } = useAppWindow();
const [section, setSectionState] = useState(lastSection);
const [query, setQuery] = useState('');
- const setSection = (s) => { lastSection = s; setSectionState(s); };
+ const setSection = (s) => {
+ if (standaloneDesktop && ['sharing', 'users', 'cooling'].includes(s)) s = 'general';
+ lastSection = s; setSectionState(s);
+ };
useEffect(() => { if (args?.section && SECTIONS.some(([id]) => id === args.section)) setSection(args.section); }, [args?.nonce]);
const [, Body] = SECTIONS.find(([id]) => id === section) || SECTIONS[0];
const q = query.trim().toLowerCase();
diff --git a/apps/store/host.jsx b/apps/store/host.jsx
new file mode 100644
index 00000000..03fa5811
--- /dev/null
+++ b/apps/store/host.jsx
@@ -0,0 +1,22 @@
+// Host package management stays with the distribution; native .lpk apps stay here.
+import { View, Text, Button, useTheme, useLocale, useLauncherApps, launchApp, spacing, typeScale } from '@lordos/sdk';
+
+export function HostPackages() {
+ const th = useTheme();
+ const { t } = useLocale();
+ const apps = useLauncherApps();
+ const manager = apps.find((a) => a.kind === 'linux' && /^(org\.gnome\.Software|org\.kde\.discover|org\.kde\.plasma-discover|synaptic)$/.test(a.desktopId));
+ return (
+
+ {t('store.host.title')}
+ {t('store.host.description')}
+ {manager && launchApp(manager.id)} />}
+
+ );
+}
+
+export const useDebian = () => ({ upgrades: [], job: null, msg: null });
+export const debian = {
+ Discover: HostPackages, Updates: HostPackages, Status: () => null,
+ canRemove: () => false,
+};
diff --git a/apps/store/index.jsx b/apps/store/index.jsx
index 97b7fec2..7cfec127 100644
--- a/apps/store/index.jsx
+++ b/apps/store/index.jsx
@@ -3,7 +3,7 @@
import {
View, Text, Image, useTheme, useLocale, useAppWindow, useStore, Button, ScrollView, SidebarItem, SectionTitle, SearchField, Hairline, EmptyState, GroupBox,
packages, packageList, refreshPackages, useLauncherApps, launchApp, fs, formatBytes, wm, direction, typeScale, spacing, isSystemApp,
- suggestions, unseenSuggestions, refreshSuggestions,
+ suggestions, unseenSuggestions, refreshSuggestions, standaloneDesktop,
} from '@lordos/sdk';
import { useEffect, useMemo, useState } from 'react';
import { debian, useDebian } from './debian.js';
@@ -129,7 +129,7 @@ function findPackageFiles() {
for (const dir of ['', 'Descargas', 'Downloads', 'Escritorio', 'Desktop']) {
let list = [];
try { list = fs.list(dir || '.'); } catch (e) { continue; }
- for (const e of list) if (!e.dir && /\.(lpk|deb)$/i.test(e.name)) out.push({ path: `${fs.home()}/${dir ? `${dir}/` : ''}${e.name}`, name: e.name, size: e.size });
+ for (const e of list) if (!e.dir && (standaloneDesktop ? /\.lpk$/i : /\.(lpk|deb)$/i).test(e.name)) out.push({ path: `${fs.home()}/${dir ? `${dir}/` : ''}${e.name}`, name: e.name, size: e.size });
}
return out;
}
@@ -144,6 +144,7 @@ function InstallFile({ path, onDone }) {
const [busy, setBusy] = useState(false);
useEffect(() => {
setMsg(null);
+ if (standaloneDesktop && isDeb) { setInfo(null); setMsg({ ok: false, text: tr('store.host.description') }); return; }
if (isDeb) { setInfo(null); return; }
try { setInfo(packages.inspect(path)); } catch (e) { setInfo(null); setMsg({ ok: false, text: tr('store.lordos.invalid', { error: e.message }) }); }
}, [path]);
@@ -152,7 +153,7 @@ function InstallFile({ path, onDone }) {
const loc = (v) => (v && typeof v === 'object' ? v[lang] || v[lang.split('-')[0]] || v.en || Object.values(v)[0] : v) || '';
const installed = m && packageList.get().find((p) => p.id === m.id);
const install = () => {
- if (isDeb) {
+ if (isDeb && !standaloneDesktop) {
setBusy(true);
debian.installFile(path).then(() => { setMsg({ ok: true, text: tr('store.done.installed') }); onDone?.(); },
(e) => setMsg({ ok: false, text: e.message })).finally(() => setBusy(false));
@@ -185,11 +186,11 @@ function InstallFile({ path, onDone }) {
)}
- {isDeb && {tr('store.file.deb')} }
+ {isDeb && !standaloneDesktop && {tr('store.file.deb')} }
onDone?.(true)} />
- {(m || isDeb) && !msg?.ok && !busy && install()} />}
+ {(m || (isDeb && !standaloneDesktop)) && !msg?.ok && !busy && install()} />}
);
diff --git a/assets/locales/en/settings.json b/assets/locales/en/settings.json
index 8f157d26..1a24f34b 100644
--- a/assets/locales/en/settings.json
+++ b/assets/locales/en/settings.json
@@ -1,4 +1,5 @@
{
+ "host.adminUnavailable": "System settings are managed by your distribution.",
"admin.langpack": "Installing packages",
"admin.locale-gen": "Generating region",
"admin.running": "{action}: {arg}…",
diff --git a/assets/locales/en/store.json b/assets/locales/en/store.json
index 8d07f79d..0bdb8238 100644
--- a/assets/locales/en/store.json
+++ b/assets/locales/en/store.json
@@ -1,4 +1,7 @@
{
+ "host.title": "System applications",
+ "host.description": "Install and update system applications with your distribution’s software manager. Native .lpk applications can be managed here.",
+ "host.open": "Open software manager",
"back": "Back",
"cancel": "Cancel",
"cat.AudioVideo": "Audio & video",
diff --git a/assets/locales/es/settings.json b/assets/locales/es/settings.json
index ba34e601..9c6a5d1a 100644
--- a/assets/locales/es/settings.json
+++ b/assets/locales/es/settings.json
@@ -1,4 +1,5 @@
{
+ "host.adminUnavailable": "Tu distribución gestiona la configuración del sistema.",
"admin.langpack": "Instalando paquetes",
"admin.locale-gen": "Generando región",
"admin.running": "{action}: {arg}…",
diff --git a/assets/locales/es/store.json b/assets/locales/es/store.json
index b659ea05..914859be 100644
--- a/assets/locales/es/store.json
+++ b/assets/locales/es/store.json
@@ -1,4 +1,7 @@
{
+ "host.title": "Aplicaciones del sistema",
+ "host.description": "Instala y actualiza las aplicaciones del sistema con el gestor de software de tu distribución. Las aplicaciones nativas .lpk se gestionan aquí.",
+ "host.open": "Abrir gestor de software",
"back": "Volver",
"cancel": "Cancelar",
"cat.AudioVideo": "Audio y video",
diff --git a/assets/locales/pt/settings.json b/assets/locales/pt/settings.json
index a561538c..04eb1ba4 100644
--- a/assets/locales/pt/settings.json
+++ b/assets/locales/pt/settings.json
@@ -1,4 +1,5 @@
{
+ "host.adminUnavailable": "As configurações do sistema são gerenciadas pela sua distribuição.",
"admin.langpack": "Instalando pacotes",
"admin.locale-gen": "Gerando região",
"admin.running": "{action}: {arg}…",
diff --git a/assets/locales/pt/store.json b/assets/locales/pt/store.json
index 4b67b844..a1095cd1 100644
--- a/assets/locales/pt/store.json
+++ b/assets/locales/pt/store.json
@@ -1,4 +1,7 @@
{
+ "host.title": "Aplicativos do sistema",
+ "host.description": "Instale e atualize os aplicativos do sistema pelo gerenciador de software da sua distribuição. Aplicativos nativos .lpk podem ser gerenciados aqui.",
+ "host.open": "Abrir gerenciador de software",
"back": "Voltar",
"cancel": "Cancelar",
"cat.AudioVideo": "Áudio e vídeo",
diff --git a/docs/STANDALONE.md b/docs/STANDALONE.md
new file mode 100644
index 00000000..de03518d
--- /dev/null
+++ b/docs/STANDALONE.md
@@ -0,0 +1,69 @@
+# Desktop independente
+
+## Objetivo e limites
+
+A branch `feature/standalone-fedora-de` separa a unidade instalável do desktop da distribuição LordOS. O alvo inicial é Fedora 44; as interfaces usadas pela DE são de Linux/Wayland e não selecionam um backend a partir do nome Debian ou Fedora.
+
+```mermaid
+flowchart TD
+ Apps[Aplicativos e temas React] --> SDK[SDK e runtime]
+ SDK --> Server[lord-server: compositor e motor gráfico]
+ Server --> Seat[libseat]
+ Seat --> Host[logind ou seatd da distribuição]
+ Server --> Services[PipeWire/PulseAudio, BlueZ, CUPS, D-Bus]
+ Store[Loja: aplicativos .lpk] --> Software[Gerenciador de software da distribuição]
+ Legacy[Perfil os: pacotes Debian, instalador, rede própria] -. fora da instalação .-> Server
+```
+
+O perfil `desktop` é padrão tanto no Makefile C quanto no compilador dos temas. O frontend usa uma capacidade de build, `standaloneDesktop`, em vez de inferir suporte à administração a partir de `/etc/os-release`. `lord.pkg` e `lord.imager` não são expostos no perfil independente. A API administrativa retorna indisponível e nenhum helper de root é criado.
+
+## Alterações implementadas
+
+| Área | Desktop independente |
+|---|---|
+| Pacotes | O resolver Debian, índices, dpkg e ações de instalação ficam fora do binário. A loja conserva `.lpk` e oferece abrir GNOME Software/Discover quando encontrados. |
+| Rede | Apenas observação por netlink; os drivers administrativos de DHCP, Wi-Fi e VPN do LordOS não são ligados ao compositor. Não há `net-restore` ao iniciar. |
+| Instalador e imagens | Instalador e Imager ficam fora dos temas e do binário. Scripts de ISO e Raspberry Pi não entram no RPM. |
+| Login | Entrada Wayland para o gerenciador existente; não instala nem habilita `lordos.service`. |
+| Dispositivos | DRM e evdev são abertos por libseat. Revogação da sessão libera input e GPU antes da confirmação; a reativação reabre os dispositivos e reutiliza o fluxo de reconexão do compositor. |
+| Teclado/mouse | Hotplug por inotify no desktop independente; a troca de TTY usa libseat. |
+| Localização | Lê `/etc/locale.conf` além do caminho legado. Na ausência de `SUPPORTED`, enumera locales instalados usando `locale -a`. Não executa locale-gen ou apt. |
+| Instalação | Suporta `PREFIX`, `LIBEXECDIR` e `DESTDIR`; instala apenas compositor, helpers de aplicativos, recursos e entrada de sessão. |
+| Helpers opcionais | Documentos: Poppler/libarchive; mídia: FFmpeg. São detectados no build. Vault é compilado; Mail depende de OpenSSL. |
+
+SHA-256 e o leitor xz ainda ficam em `server/src/pkg/crypto.c` e `xz.c`, porque também são utilizados por assinaturas, cofre, journal e mapas. Esses arquivos são primitivas compartilhadas, não um backend de pacotes Debian. Movê-los para uma pasta comum pode ser uma refatoração posterior.
+
+## Verificação
+
+`make check` compila o desktop, executa testes do ciclo de vida da sessão com um provedor simulado, verifica que nenhuma ação administrativa funciona e examina os símbolos do binário para impedir a ligação acidental de componentes do SO.
+
+Também valida os hashes dos bundles, instala em diretório temporário, verifica que nenhum serviço ou configuração de `/etc` foi instalado, confere o manifesto após a instalação e inicializa os temas sem monitor como usuário sem privilégios.
+
+No Fedora, instale `libasan` e `libubsan` para os testes com sanitização. `make -C server test` executa a suíte original. Parte dela testa componentes legados isoladamente; isso não significa que esses componentes entram no desktop. Testes dependentes de ferramentas ou dados externos podem ser pulados pela suíte upstream.
+
+A CI `.github/workflows/desktop-fedora.yml` reproduz o build e os testes em Fedora 44. O modo headless não exercita DRM/KMS, input físico, aceleração de GPU nem políticas SELinux.
+
+## Compatibilidade ainda a validar
+
+1. Login real por GDM/SDDM e TTY; seleção da GPU em máquinas com mais de uma placa.
+2. Troca e retorno de TTY, suspensão, remoção de monitores e hotplug de periféricos.
+3. Aceleração Mesa e Xwayland no Fedora, incluindo GPUs diferentes.
+4. Helpers de mídia/documentos com as versões de FFmpeg/Poppler fornecidas pelo host.
+5. Comportamento sob SELinux em enforcing, portais e ativação de aplicativos D-Bus.
+6. Aplicativos isolados em sessão de usuário: delegação de cgroups e permissões que antes dependiam dos serviços LordOS.
+
+A separação remove a necessidade dos serviços específicos do LordOS, mas não substitui automaticamente todas as suas funções. O firewall por aplicativo de `lord-netperm`, o controle de ventoinhas e a administração privilegiada não fazem parte desta instalação.
+
+## Evolução
+
+- Integrar NetworkManager por D-Bus e autorização via polkit, sem assumir controle da rede do host.
+- Substituir as páginas administrativas restantes por adaptadores de serviços do host.
+- Publicar um RPM com build reproduzível do frontend conforme as regras de empacotamento do destino, dependências opcionais e assinatura própria do fork.
+- Extrair ou remover os diretórios legados da distribuição após estabilizar a sessão independente.
+- Expandir a matriz de testes para outras distribuições e arquiteturas.
+
+## Referências de implementação
+
+- [API libseat](https://github.com/kennylevinsen/seatd/blob/master/include/libseat.h) e [exemplo oficial](https://github.com/kennylevinsen/seatd/blob/master/examples/simpletest/main.c).
+- [Pacote libseat no Fedora](https://packages.fedoraproject.org/pkgs/seatd/libseat-devel/).
+- [Wayland no Fedora](https://packages.fedoraproject.org/pkgs/wayland/wayland-devel/).
diff --git a/package-lock.json b/package-lock.json
index 9aca2f25..a8d5b5ff 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,11 +1,11 @@
{
- "name": "lordos",
+ "name": "lord-de",
"version": "0.8.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "lordos",
+ "name": "lord-de",
"version": "0.8.3",
"dependencies": {
"react": "19.3.1",
diff --git a/package.json b/package.json
index 5fb1f4c5..1abef8ab 100644
--- a/package.json
+++ b/package.json
@@ -1,10 +1,10 @@
{
- "name": "lordos",
+ "name": "lord-de",
"private": true,
"version": "0.8.3",
- "description": "LordOS — React themes and apps for lord-server",
+ "description": "Lord DE — independent Wayland desktop derived from LordOS",
"lordos": {
- "channel": "beta"
+ "channel": "dev"
},
"scripts": {
"build": "node tools/build-theme.mjs",
diff --git a/packaging/desktop/lord-de.desktop.in b/packaging/desktop/lord-de.desktop.in
new file mode 100644
index 00000000..da095dd3
--- /dev/null
+++ b/packaging/desktop/lord-de.desktop.in
@@ -0,0 +1,6 @@
+[Desktop Entry]
+Name=Lord DE (experimental)
+Comment=Independent Wayland desktop derived from LordOS
+Exec=@BINDIR@/lord-de-session
+TryExec=@BINDIR@/lord-de-session
+Type=Application
diff --git a/packaging/rpm/lord-de.spec b/packaging/rpm/lord-de.spec
new file mode 100644
index 00000000..97aec10b
--- /dev/null
+++ b/packaging/rpm/lord-de.spec
@@ -0,0 +1,60 @@
+Name: lord-de
+Version: 0.8.3
+Release: 1%{?dist}
+Summary: Independent Wayland desktop derived from LordOS
+License: MIT AND BSD-2-Clause AND CC0-1.0 AND OFL-1.1 AND CC-BY-4.0 AND CC-BY-3.0-US
+URL: https://github.com/lordbasex/LordOS
+Source0: %{name}-%{version}.tar.gz
+
+BuildRequires: gcc
+BuildRequires: make
+BuildRequires: python3
+BuildRequires: binutils
+BuildRequires: pkgconfig(wayland-server)
+BuildRequires: pkgconfig(wayland-client)
+BuildRequires: pkgconfig(wayland-protocols)
+BuildRequires: pkgconfig(xkbcommon)
+BuildRequires: pkgconfig(libseat)
+BuildRequires: pkgconfig(openssl)
+BuildRequires: libxcrypt-devel
+Requires: dbus-daemon
+Requires: xkeyboard-config
+Requires: libX11-common
+Requires: tzdata
+Recommends: pipewire-pulseaudio
+Recommends: xorg-x11-server-Xwayland
+Recommends: polkit
+
+%description
+Lord DE packages the LordOS compositor, React desktop and native applications
+as an independent desktop. Host networking, packages and login are managed by
+the distribution. This experimental package adds a Wayland session entry.
+It does not replace the display manager or enable system services.
+
+%prep
+%autosetup
+
+%build
+# dist.sh includes the frontend built from package-lock.json. No network access
+# or Node.js runtime is needed to compile or run this local RPM.
+%set_build_flags
+%make_build -C server PROFILE=desktop GPU=0
+
+%install
+python3 tools/desktop/install.py --prefix %{_prefix} --libexecdir %{_libexecdir} --destdir %{buildroot}
+
+%check
+%make_build -C server PROFILE=desktop GPU=0 test-platform
+python3 tools/desktop/check.py
+
+%files
+%license LICENSE
+%{_bindir}/lord-de-session
+%{_libexecdir}/lord-de/
+%{_datadir}/wayland-sessions/lord-de.desktop
+%{_datadir}/doc/lord-de/
+%{_datadir}/licenses/lord-de/third_party/
+
+%changelog
+* Fri Oct 23 2026 Lord DE contributors - 0.8.3-1
+- Extract an independent desktop profile with a Fedora session package.
diff --git a/sdk/index.js b/sdk/index.js
index 2d7567b7..25ef639a 100644
--- a/sdk/index.js
+++ b/sdk/index.js
@@ -1,4 +1,5 @@
// @lordos/sdk — everything a LordOS theme or app needs.
+export { standaloneDesktop } from './platform.js';
import { createElement, cloneElement, Fragment } from 'react';
import { render, onScreenChange, addListener } from '@lordos/runtime';
import { settings } from './settings.js';
diff --git a/sdk/platform.js b/sdk/platform.js
new file mode 100644
index 00000000..d88516d8
--- /dev/null
+++ b/sdk/platform.js
@@ -0,0 +1,2 @@
+// Build capability, independent of the host distribution ID.
+export const standaloneDesktop = typeof __LORD_STANDALONE__ !== 'undefined' && __LORD_STANDALONE__;
diff --git a/sdk/smart.js b/sdk/smart.js
index 637c04aa..c985ff11 100644
--- a/sdk/smart.js
+++ b/sdk/smart.js
@@ -13,6 +13,7 @@
// All of it runs as the root helper's closed smart-* actions: fixed /usr/sbin/smartctl, a validated
// whole disk, JSON back in LOG lines. Without smartmontools the summary comes from LordOS's own
// reader (disk-smart), only for disks that will not be woken up by it.
+import { standaloneDesktop } from './platform.js';
import { admin } from './admin.js';
import { createStore, useStore } from './store.js';
import { settings } from './settings.js';
@@ -42,6 +43,7 @@ export const smart = {
smartable,
installed: () => { try { return hasLord() && !!lord.disks.tools().smartctl; } catch (e) { return false; } },
install(onLog) {
+ if (standaloneDesktop) return Promise.reject(codeError('admin.unavailable'));
const go = () => admin.run('pkg-install', SMART_PACKAGE, onLog);
return go().catch((e) => {
if (e.code !== 'pkg.noIndex') throw e; // no package lists yet (fresh install): fetch them once
diff --git a/sdk/updates.js b/sdk/updates.js
index a85acde7..0c6e5956 100644
--- a/sdk/updates.js
+++ b/sdk/updates.js
@@ -7,6 +7,7 @@
// The packages come from the signed LordOS repository on lordos.org (and Debian's), through our own
// package engine (lord.pkg, admin.run('pkg-update' | 'pkg-install' | 'pkg-upgrade')). The optional
// daily check only downloads the signed lists and notifies; it never installs anything.
+import { standaloneDesktop } from './platform.js';
import { settings } from './settings.js';
import { admin } from './admin.js';
import { notify } from './notify.js';
@@ -14,7 +15,7 @@ import { t } from './i18n.js';
import { wm } from './windows.js';
import { lordosVersion } from './system.js';
-const has = () => typeof lord !== 'undefined' && !!lord.pkg && typeof lord.pkg.rebootRequired === 'function';
+const has = () => !standaloneDesktop && typeof lord !== 'undefined' && !!lord.pkg && typeof lord.pkg.rebootRequired === 'function';
export const UPDATE_DEFAULTS = { autoCheck: true, notified: '' };
export const updatePrefs = (s = settings.get()) => ({ ...UPDATE_DEFAULTS, ...(s.updates || {}) });
diff --git a/server/Makefile b/server/Makefile
index 358391e3..987398dd 100644
--- a/server/Makefile
+++ b/server/Makefile
@@ -1,6 +1,22 @@
# lord-server — libc + kernel headers + libxkbcommon + libwayland-server + vendored QuickJS and stb.
# No libdrm, libinput, X11 or WebKit; the Wayland compositor (src/wl) and the terminal (src/term) are our own.
CC ?= cc
+PROFILE ?= desktop
+ifneq ($(filter $(PROFILE),desktop os),$(PROFILE))
+$(error PROFILE must be desktop or os)
+endif
+ifeq ($(PROFILE),desktop)
+CPPFLAGS += -DLORD_STANDALONE
+SESSIOND := 0
+BPF := 0
+SEAT ?= 1
+else
+SEAT ?= 0
+endif
+ifeq ($(SEAT),1)
+CPPFLAGS += -DLORD_LIBSEAT $(shell pkg-config --cflags libseat)
+LDLIBS += $(shell pkg-config --libs libseat)
+endif
CFLAGS ?= -O2 -g
WARN := -Wall -Wextra
CPPFLAGS += -D_GNU_SOURCE -Isrc -Ibuild/gen -isystem ../third_party/quickjs -isystem ../third_party/stb
@@ -19,7 +35,7 @@ endif
endif
# Wayland protocols: wayland-scanner generates the code from the wayland-protocols XML files.
-WLPROTO := /usr/share/wayland-protocols
+WLPROTO ?= $(shell pkg-config --variable=pkgdatadir wayland-protocols)
WL_XML := $(WLPROTO)/stable/xdg-shell/xdg-shell.xml $(WLPROTO)/unstable/xdg-decoration/xdg-decoration-unstable-v1.xml
WL_GEN := build/gen/xdg-shell-protocol.h build/gen/xdg-decoration-unstable-v1-protocol.h
WL_OBJ := build/gen/xdg-shell-protocol.o build/gen/xdg-decoration-unstable-v1-protocol.o
@@ -72,7 +88,7 @@ SRC += $(DOC_SRC)
FW_SRC := src/fw/rules.c src/fw/dnsmap.c src/fw/fwmap.c src/ui/js_fw.c
SRC += $(FW_SRC)
OBJ := $(SRC:src/%.c=build/obj/%.o)
-HDR := $(wildcard src/*.h src/gpu/*.h src/ui/*.h src/term/*.h src/wl/*.h src/wasm/*.h src/sound/*.h src/pkg/*.h src/net/*.h src/install/*.h src/remote/*.h src/users/*.h src/sec/*.h src/monitor/*.h src/media/*.h src/app/*.h src/doc/*.h src/print/*.h) $(WL_GEN) $(APP_CLIENT_HDR)
+HDR := $(wildcard src/*.h src/gpu/*.h src/ui/*.h src/term/*.h src/wl/*.h src/wasm/*.h src/sound/*.h src/pkg/*.h src/net/*.h src/install/*.h src/remote/*.h src/users/*.h src/sec/*.h src/platform/*.h src/monitor/*.h src/media/*.h src/app/*.h src/doc/*.h src/print/*.h) $(WL_GEN) $(APP_CLIENT_HDR)
HDR += $(wildcard src/fw/*.h)
# Touchpad and touchscreens: our own driver (src/tp, no libinput), gestures and touches for the
# Wayland apps (src/wl/wl_input.c) and lord.input (src/ui/js_input.c). See docs/TRACKPAD.md.
@@ -126,10 +142,30 @@ build/imager-test: tests/imager_test.c src/imager/imgverify.c src/imager/imgveri
$(CC) $(CPPFLAGS) -O1 -g $(WARN) -std=c11 -fsanitize=address,undefined -fno-sanitize-recover=all -fno-omit-frame-pointer \
-o $@ tests/imager_test.c src/imager/imgverify.c src/pkg/crypto.c
./$@ || { rm -f $@; exit 1; }
+ifeq ($(PROFILE),desktop)
+# Distribution administration lives in the legacy OS profile. crypto.c and xz.c
+# are shared primitives used by the vault, signatures, journal and map reader.
+SRC := $(filter-out src/admin.c src/install/%.c src/imager/%.c src/ui/js_imager.c src/ui/js_pkg.c \
+ src/pkg/deb.c src/pkg/idx.c src/pkg/status.c src/pkg/resolve.c src/pkg/http.c src/pkg/pkg.c src/pkg/pgp.c \
+ src/net/crypto.c src/net/sys.c src/net/dhcp.c src/net/wifi.c src/net/wg.c src/net/ovpn.c \
+ src/net/vpn.c src/net/vici.c src/net/ipsec.c src/net/vpnproc.c src/net/actions.c src/remote/remote_admin.c src/remote/ssh_admin.c src/users/users_admin.c \
+ src/disk/disk_admin.c src/disk/smartctl_admin.c src/logs/logs_admin.c src/monitor/proc_admin.c,$(SRC))
+SRC += src/platform/admin.c src/platform/net.c
+endif
+SRC += src/platform/seat.c
OBJ := $(SRC:src/%.c=build/obj/%.o)
HDR += $(wildcard src/tp/*.h)
-.PHONY: all clean test test-full
+.PHONY: all clean test test-full test-platform
+test-platform: build/seat-test build/platform-admin-test
+ ./build/seat-test
+ ./build/platform-admin-test
+build/seat-test: tests/seat_test.c src/platform/seat.c src/platform/seat.h
+ @mkdir -p build
+ $(CC) $(CFLAGS) $(WARN) -D_GNU_SOURCE -DLORD_LIBSEAT $(shell pkg-config --cflags libseat) -Isrc -o $@ tests/seat_test.c src/platform/seat.c
+build/platform-admin-test: tests/platform_admin_test.c src/platform/admin.c src/admin.h
+ @mkdir -p build
+ $(CC) $(CFLAGS) $(WARN) -D_GNU_SOURCE -Isrc -o $@ tests/platform_admin_test.c src/platform/admin.c
# lord-sessiond: sessions, login screen and remote desktop (docs/SESSIONS.md). It's the
# only one that links OpenSSL (TLS), and with it libsrtp2 and usrsctp (the remote desktop's WebRTC);
# without libssl-dev, only lord-server is built.
@@ -158,7 +194,9 @@ endif
BPF ?= $(shell command -v clang >/dev/null 2>&1 && echo 1 || echo 0)
NP_SRC := src/netperm/netperm.c src/netperm/bpfsys.c src/netperm/npcompile.c src/netperm/npdns.c src/netperm/npdiag.c \
src/fw/rules.c src/fw/dnsmap.c src/fw/fwmap.c src/sec/json.c
+ifeq ($(PROFILE),os)
ALL += build/lord-netperm
+endif
ifeq ($(BPF),1)
NP_GEN := build/gen/np_bpf.h
NP_CPP := -DNP_HAVE_BPF
@@ -192,7 +230,9 @@ build/npcompile-test: tests/npcompile_test.c src/netperm/npcompile.c src/netperm
# only; it executes nothing and has no network. tests/fan_test.c: curves, fan.json, requests and a
# fake sysfs (run by tests/run_suites.sh).
FAN_SRC := src/fan/fancurve.c src/fan/fanhw.c src/sec/json.c
+ifeq ($(PROFILE),os)
ALL += build/lord-fand
+endif
build/lord-fand: src/fan/fand.c $(FAN_SRC) $(wildcard src/fan/*.h) src/sec/json.h
@mkdir -p build
$(CC) $(CPPFLAGS) $(CFLAGS) $(WARN) -std=c11 -o $@ src/fan/fand.c $(FAN_SRC)
@@ -536,6 +576,13 @@ build/obj/%.o: src/%.c $(HDR)
@mkdir -p $(dir $@)
$(CC) $(CPPFLAGS) $(CFLAGS) $(WARN) -std=c11 -c -o $@ $<
+# Switching profiles must rebuild objects even without `make clean`.
+build/gen/profile.h: FORCE
+ @mkdir -p build/gen
+ @printf '/* profile=%s seat=%s gpu=%s */\n' '$(PROFILE)' '$(SEAT)' '$(GPU)' > $@.new
+ @if cmp -s $@.new $@; then rm -f $@.new; else mv $@.new $@; fi
+$(OBJ): build/gen/profile.h
+
# Third-party code: compiled as is, without our warnings.
build/obj/ui/stb.o: src/ui/stb.c
@mkdir -p $(dir $@)
@@ -575,7 +622,9 @@ include $(sort $(wildcard mk/*.mk))
# lord-netd (docs/NETWORK.md, "The network belongs to the system"): admin.c with LORD_NETD (net-* actions
# only) plus networking and the package engine (net-vpn-enable). libc only; lord-sessiond runs it.
+ifeq ($(PROFILE),os)
all: build/lord-netd
+endif
NETD_OBJ := build/obj/netd/admin.o build/obj/netd/netd.o $(NET_SRC:src/%.c=build/obj/%.o) $(PKG_SRC:src/%.c=build/obj/%.o) build/obj/ui/stb.o
build/obj/netd/admin.o: src/admin.c $(HDR)
@mkdir -p $(dir $@)
diff --git a/server/src/drmcard.h b/server/src/drmcard.h
index 646dd219..57a78bbb 100644
--- a/server/src/drmcard.h
+++ b/server/src/drmcard.h
@@ -21,6 +21,9 @@
#include
#include
#include
+#ifdef LORD_LIBSEAT
+#include "platform/seat.h"
+#endif
/* 2 = has a connected connector, 1 = has connectors (none connected), 0 = not KMS. */
static inline int drm_card_score(int fd) {
@@ -56,10 +59,19 @@ static inline int drm_pick_card(char *out, size_t size) {
for (int i = 0; i < 16 && best < 2; i++) {
char p[32];
snprintf(p, sizeof p, "/dev/dri/card%d", i);
+ if (access(p, F_OK)) continue;
+#ifdef LORD_LIBSEAT
+ int fd = seat_device_open(p, O_RDWR | O_CLOEXEC);
+#else
int fd = open(p, O_RDWR | O_CLOEXEC);
+#endif
if (fd < 0) continue;
int s = drm_card_score(fd);
+#ifdef LORD_LIBSEAT
+ seat_device_close(fd);
+#else
close(fd);
+#endif
if (s > best) { best = s; best_i = i; }
}
snprintf(out, size, "/dev/dri/card%d", best_i < 0 ? 0 : best_i);
diff --git a/server/src/input.c b/server/src/input.c
index 16b4f65d..8ad144ca 100644
--- a/server/src/input.c
+++ b/server/src/input.c
@@ -1,5 +1,6 @@
/* input.c — evdev device detection and event reading. */
#include "input.h"
+#include "platform/seat.h"
#include "tp/tp.h"
#include
@@ -7,6 +8,8 @@
#include
#include
#include
+#include
+#include
#include
#define LONG_BITS (8 * sizeof(long))
@@ -25,7 +28,7 @@ static void add_device(Input *in, int fd, const char *path) {
int keyboard = HAS(ev, EV_KEY) && HAS(keys, KEY_A) && HAS(keys, KEY_ENTER);
int mouse = HAS(ev, EV_REL) && HAS(keys, BTN_LEFT);
int tablet = HAS(ev, EV_ABS) && HAS(abs, ABS_X) && HAS(abs, ABS_Y);
- if ((!keyboard && !mouse && !tablet) || in->count >= INPUT_MAX_DEVICES) { close(fd); return; }
+ if ((!keyboard && !mouse && !tablet) || in->count >= INPUT_MAX_DEVICES) { seat_device_close(fd); return; }
InputDevice *d = &in->dev[in->count++];
memset(d, 0, sizeof *d); /* the slot may have belonged to one that was unplugged */
@@ -51,17 +54,46 @@ static void add_device(Input *in, int fd, const char *path) {
: mouse ? " [mouse]" : (tablet ? " [tablet]" : ""));
}
-int input_open(Input *in) {
- memset(in, 0, sizeof *in);
+static void scan_devices(Input *in) {
for (int i = 0; i < 64 && in->count < INPUT_MAX_DEVICES; i++) {
char path[32];
snprintf(path, sizeof path, "/dev/input/event%d", i);
- int fd = open(path, O_RDONLY | O_NONBLOCK | O_CLOEXEC);
+ struct stat st, existing;
+ if (stat(path, &st)) continue;
+ int found = 0;
+ for (int k = 0; k < in->count; k++)
+ if (!fstat(in->dev[k].fd, &existing) && existing.st_rdev == st.st_rdev && existing.st_ino == st.st_ino) { found = 1; break; }
+ if (found) continue;
+ int fd = seat_device_open(path, O_RDONLY | O_NONBLOCK | O_CLOEXEC);
if (fd >= 0) add_device(in, fd, path);
}
+}
+
+int input_open(Input *in) {
+ memset(in, 0, sizeof *in);
+ scan_devices(in);
return in->count > 0 ? 0 : -1;
}
+/* lord-sessiond used to forward new devices. An independent session observes
+ * /dev/input itself; all access still goes through the seat provider. */
+static int watch = -1;
+int input_watch_fd(void) {
+ if (watch >= 0) return watch;
+ watch = inotify_init1(IN_CLOEXEC | IN_NONBLOCK);
+ if (watch >= 0 && inotify_add_watch(watch, "/dev/input", IN_CREATE | IN_MOVED_TO) < 0) {
+ close(watch); watch = -1;
+ }
+ return watch;
+}
+int input_watch_dispatch(Input *in, int active) {
+ char events[4096];
+ while (read(watch, events, sizeof events) > 0) { }
+ int before = in->count;
+ if (active) scan_devices(in);
+ return before != in->count;
+}
+
int input_open_fds(Input *in, const int *fds, int n) {
memset(in, 0, sizeof *in);
for (int i = 0; i < n; i++) {
@@ -84,7 +116,7 @@ void input_remove(Input *in, int idx) {
if (idx < 0 || idx >= in->count) return;
InputDevice *d = &in->dev[idx];
fprintf(stderr, "input: %s is gone; closing it\n", d->name);
- close(d->fd);
+ seat_device_close(d->fd);
tp_free(d->tp);
memmove(&in->dev[idx], &in->dev[idx + 1], sizeof in->dev[0] * (size_t)(in->count - idx - 1));
in->count--;
@@ -93,7 +125,7 @@ void input_remove(Input *in, int idx) {
void input_close(Input *in) {
for (int i = 0; i < in->count; i++) {
ioctl(in->dev[i].fd, EVIOCGRAB, 0);
- close(in->dev[i].fd);
+ seat_device_close(in->dev[i].fd);
tp_free(in->dev[i].tp);
in->dev[i].tp = NULL;
}
diff --git a/server/src/input.h b/server/src/input.h
index 69c5606e..10e4e109 100644
--- a/server/src/input.h
+++ b/server/src/input.h
@@ -23,6 +23,8 @@ typedef struct {
typedef void (*InputHandler)(void *ud, const InputDevice *dev, const struct input_event *ev);
int input_open(Input *in); /* opens and grabs (EVIOCGRAB) the useful devices */
+int input_watch_fd(void); /* inotify: standalone session hotplug */
+int input_watch_dispatch(Input *in, int active);
/* Same, with fds already opened and passed by lord-sessiond (useless ones are closed). */
int input_open_fds(Input *in, const int *fds, int n);
void input_close(Input *in);
diff --git a/server/src/kms.c b/server/src/kms.c
index 221eecfe..d31bace1 100644
--- a/server/src/kms.c
+++ b/server/src/kms.c
@@ -1,5 +1,6 @@
/* kms.c — modesetting with kernel ioctls: connector, CRTC, dumb buffers, page flip. */
#include "kms.h"
+#include "platform/seat.h"
#include
#include
@@ -354,9 +355,9 @@ int kms_head_open(Kms *k, int fd, uint32_t conn_id, const struct drm_mode_modein
int kms_open(Kms *k, const char *path) {
memset(k, 0, sizeof *k);
- k->fd = open(path, O_RDWR | O_CLOEXEC);
+ k->fd = seat_device_open(path, O_RDWR | O_CLOEXEC);
if (k->fd < 0) { perror(path); return -1; }
- if (xioctl(k->fd, DRM_IOCTL_SET_MASTER, NULL))
+ if (!seat_managed() && xioctl(k->fd, DRM_IOCTL_SET_MASTER, NULL))
fprintf(stderr, "kms: warning, we are not DRM master (%s)\n", strerror(errno));
if (kms_setup(k, 0)) { kms_close(k); return -1; }
return 0;
@@ -396,8 +397,8 @@ void kms_close(Kms *k) {
destroy_buffer(k, &k->buf[0]);
destroy_buffer(k, &k->buf[1]);
if (k->shared) { k->fd = -1; return; } /* the fd belongs to the main head */
- if (!k->borrowed) xioctl(k->fd, DRM_IOCTL_DROP_MASTER, NULL);
- close(k->fd);
+ if (!k->borrowed && !seat_managed()) xioctl(k->fd, DRM_IOCTL_DROP_MASTER, NULL);
+ seat_device_close(k->fd);
k->fd = -1;
}
diff --git a/server/src/main.c b/server/src/main.c
index 9c9e650d..51870b10 100644
--- a/server/src/main.c
+++ b/server/src/main.c
@@ -16,6 +16,7 @@
* recuperan en el próximo arranque; las dos primeras formas los cierran.
*/
#include "admin.h"
+#include "platform/seat.h"
#include "keylag.h"
#include "net/net.h"
#include "disk/disk.h"
@@ -727,7 +728,7 @@ static void outputs_apply(Server *s) {
/* lord-sessiond: a keyboard, mouse or touchpad plugged in while the session is on the monitor */
static void remote_input_add(const int *fds, int n) {
Server *s = remote_server;
- if (s->headless) { for (int i = 0; i < n; i++) close(fds[i]); return; }
+ if (s->headless) { for (int i = 0; i < n; i++) seat_device_close(fds[i]); return; }
if (input_add_fds(&s->input, fds, n)) wl_input_devices_changed();
}
@@ -738,7 +739,7 @@ static void remote_input_add(const int *fds, int n) {
static int parked_fd[PARK_MAX], parked_n, parked_flags;
static unsigned parked_seq;
static void parked_drop(void) {
- for (int i = 0; i < parked_n; i++) close(parked_fd[i]);
+ for (int i = 0; i < parked_n; i++) seat_device_close(parked_fd[i]);
parked_n = 0;
}
@@ -746,10 +747,12 @@ static void parked_drop(void) {
* the first frame is on the screen (docs/SESSIONS.md, "Boot splash"). */
static int splash_wait;
+static char local_card[PATH_MAX];
+
static void display_attach(const int *fds, int n, int flags) {
Server *s = remote_server;
parked_drop(); /* a new RP_DISPLAY replaces what was waiting */
- if (!s->headless || n < 1) { for (int i = 0; i < n; i++) close(fds[i]); return; }
+ if (!s->headless || n < 1) { for (int i = 0; i < n; i++) seat_device_close(fds[i]); return; }
Out *o0 = &s->out[0];
out_free(o0);
memset(o0, 0, sizeof *o0);
@@ -768,7 +771,7 @@ static void display_attach(const int *fds, int n, int flags) {
} else {
fprintf(stderr, "monitor: could not use the card\n");
if (card >= 0) close(card);
- for (int i = 1; i < n; i++) close(fds[i]);
+ for (int i = 1; i < n; i++) seat_device_close(fds[i]);
}
set_size(s, s->W, s->H); /* back to the frame in memory */
return;
@@ -831,6 +834,22 @@ static void display_detach(void) {
fprintf(stderr, "monitor: the session leaves the screen\n");
}
+/* Reuse the compositor's existing display hand-off on logind/seatd switches. */
+static void local_enable(void) {
+ int fds[65], n = 0;
+ int card = seat_device_open(local_card, O_RDWR | O_CLOEXEC);
+ if (card < 0) { perror("lord-de: reopening DRM"); return; }
+ fds[n++] = card;
+ for (int i = 0; i < 64; i++) {
+ char path[64];
+ snprintf(path, sizeof path, "/dev/input/event%d", i);
+ if (access(path, F_OK)) continue;
+ int fd = seat_device_open(path, O_RDONLY | O_NONBLOCK | O_CLOEXEC);
+ if (fd >= 0) fds[n++] = fd;
+ }
+ display_attach(fds, n, 0);
+}
+
static void on_input(void *ud, const InputDevice *d, const struct input_event *ev) {
Server *s = ud;
if (ev->type == EV_KEY || ev->type == EV_REL || ev->type == EV_ABS) display_activity(now_ms());
@@ -876,6 +895,10 @@ static void on_input(void *ud, const InputDevice *d, const struct input_event *e
if (ev->value == 1 && ev->code < BTN_MISC) tp_key_activity(ev->code, now_ms()); /* ignorar el touchpad al escribir */
if (ev->code == KEY_LEFTCTRL || ev->code == KEY_RIGHTCTRL) s->ctrl = ev->value != 0;
if (ev->code == KEY_LEFTALT || ev->code == KEY_RIGHTALT) s->alt = ev->value != 0;
+ if (ev->value == 1 && s->ctrl && s->alt && ev->code >= KEY_F1 && ev->code <= KEY_F10 && seat_managed()) {
+ seat_switch(ev->code - KEY_F1 + 1);
+ break;
+ }
if (ev->code == KEY_BACKSPACE && ev->value == 1 && s->ctrl && s->alt) s->quit = 1;
/* Botones con la numeración del DOM: 0 izquierdo, 1 medio, 2 derecho. */
int button = ev->code == BTN_LEFT ? 0 : ev->code == BTN_MIDDLE ? 1 : ev->code == BTN_RIGHT ? 2 : -1;
@@ -1560,6 +1583,12 @@ int main(int argc, char **argv) {
}
return app_main(argv[2], host, fonts);
}
+#ifdef LORD_STANDALONE
+ if (geteuid() == 0) {
+ fprintf(stderr, "lord-de: start the desktop as your normal session user, without sudo\n");
+ return 1;
+ }
+#endif
const char *card = NULL; /* sin --card: la que tiene pantalla (drmcard.h) */
const char *theme = "../themes/default/theme.bundle.js";
const char *fonts = "../assets/fonts";
@@ -1607,10 +1636,12 @@ int main(int argc, char **argv) {
set_size(&s, w, h); /* out[0] = the frame in memory */
}
else {
+ if (seat_start()) return 1;
char picked[64];
if (!card) { drm_pick_card(picked, sizeof picked); card = picked; }
kms_set_picker(display_pick); /* the display saved in Settings (displays.json) */
- if (kms_open(K0(&s), card)) return 1;
+ snprintf(local_card, sizeof local_card, "%s", card);
+ if (kms_open(K0(&s), card)) { seat_stop(); return 1; }
/* one display until the plan (after dropping root: displays.json is the user's) opens the others */
s.W = (int)K0(&s)->width;
s.H = (int)K0(&s)->height;
@@ -1628,18 +1659,21 @@ int main(int argc, char **argv) {
if (!s.headless) {
if (input_open(&s.input)) fprintf(stderr, "input: no devices (permissions?)\n");
/* The text console stops drawing over us. */
- vt = open("/dev/tty0", O_RDWR | O_CLOEXEC);
+ if (!seat_managed()) vt = open("/dev/tty0", O_RDWR | O_CLOEXEC);
if (vt >= 0 && ioctl(vt, KDSETMODE, KD_GRAPHICS)) { close(vt); vt = -1; }
}
/* Privileged helper (a closed set of actions: region, time zone, language packs). The
* login screen has none: what needs root is done by lord-sessiond. */
const char *session_user = s.greeter ? NULL : user ? user : getenv("SUDO_USER");
+#ifndef LORD_STANDALONE
if (session_user && admin_start(session_user)) fprintf(stderr, "lord-server: no administration helper\n");
/* LordOS networking: with lord-sessiond the network belongs to the system (lord-netd brings it
* up at boot) and net-restore only confirms; without it (a standalone lord-server) this
* session's helper brings it up (see net_act_restore and docs/NETWORK.md) */
else if (session_user) admin_send("net-restore", "", 0);
+#endif
+ if (!session_user && !s.greeter) session_user = getenv("USER");
int ok = drop_privileges(user) == 0;
if (ok && s.headless && getenv("HOME")) {
/* no terminal: the session log goes to the home folder */
@@ -1691,6 +1725,7 @@ int main(int argc, char **argv) {
input_close(&s.input);
if (!s.headless) kms_close(K0(&s));
if (vt >= 0) { ioctl(vt, KDSETMODE, KD_TEXT); close(vt); }
+ seat_stop();
return 1;
}
@@ -1703,6 +1738,7 @@ int main(int argc, char **argv) {
int sfd = signalfd(-1, &mask, SFD_CLOEXEC);
double start = now_ms();
+ seat_callbacks(local_enable, display_detach);
while (!s.quit && !ui.quit_requested) {
if (ui.theme_request[0]) {
char next[sizeof current];
@@ -1778,6 +1814,10 @@ int main(int argc, char **argv) {
pfd[n++] = (struct pollfd){ K0(&s)->fd, POLLIN, 0 };
pfd[n++] = (struct pollfd){ sfd, POLLIN, 0 };
for (int i = 0; i < s.input.count; i++) pfd[n++] = (struct pollfd){ s.input.dev[i].fd, POLLIN, 0 };
+ int seatbase = seat_fd() >= 0 ? n : -1;
+ if (seatbase >= 0) pfd[n++] = (struct pollfd){ seat_fd(), POLLIN, 0 };
+ int inputbase = seat_managed() && input_watch_fd() >= 0 ? n : -1;
+ if (inputbase >= 0) pfd[n++] = (struct pollfd){ input_watch_fd(), POLLIN, 0 };
int tbase = n, nterm = term_pollfds(pfd + n, TERM_MAX, term_ids);
n += nterm;
int abase = admin_fd() >= 0 ? n : -1;
@@ -1848,6 +1888,9 @@ int main(int argc, char **argv) {
if (nabr) brokersrv_audio_dispatch(pfd + abr, nabr);
if (nmed) media_dispatch(pfd + medbase, nmed);
if (nhw) hw_dispatch(pfd + hwbase, nhw);
+ /* Dispatch after input: a seat switch can replace the entire device list. */
+ if (seatbase >= 0 && pfd[seatbase].revents && seat_dispatch() < 0) break;
+ if (inputbase >= 0 && pfd[inputbase].revents && input_watch_dispatch(&s.input, !s.headless)) wl_input_devices_changed();
if (parked_n && s.headless && display_hotplug_seq() != parked_seq) { /* a monitor may have appeared */
int f[PARK_MAX], fn = parked_n;
memcpy(f, parked_fd, sizeof(int) * (size_t)fn);
@@ -1871,6 +1914,7 @@ int main(int argc, char **argv) {
if (s.renders)
fprintf(stderr, "lord-server: %ld frames, %ld UI renders, layout %.2f ms, paint %.2f ms (average)\n",
s.frames, s.renders, s.layout_total / s.renders, s.paint_total / s.renders);
+ seat_callbacks(NULL, NULL);
input_close(&s.input);
mirror_stop();
gpu_tex_free(s.cursor_tex);
@@ -1886,5 +1930,6 @@ int main(int argc, char **argv) {
if (vt >= 0) { ioctl(vt, KDSETMODE, KD_TEXT); close(vt); }
for (int i = 0; i < OUT_MAX; i++) out_free(&s.out[i]);
out_free(&s.cap);
+ seat_stop();
return 0;
}
diff --git a/server/src/net/state.c b/server/src/net/state.c
index 8e49e4cc..760acaee 100644
--- a/server/src/net/state.c
+++ b/server/src/net/state.c
@@ -258,6 +258,9 @@ char *net_interfaces_json(void) {
char conf[128];
snprintf(conf, sizeof conf, NET_IF_DIR "/%s.conf", k->name);
const char *managed = managed_by_system(k->name) ? "system" : access(conf, F_OK) == 0 ? "lordos" : "none";
+#ifdef LORD_STANDALONE
+ managed = "system";
+#endif
if (!strcmp(type, "wireguard") || !strcmp(type, "tun")) {
/* those of LordOS's VPNs */
char meta[128];
diff --git a/server/src/platform/admin.c b/server/src/platform/admin.c
new file mode 100644
index 00000000..d33388e2
--- /dev/null
+++ b/server/src/platform/admin.c
@@ -0,0 +1,30 @@
+/* Standalone desktop: system administration belongs to the host distribution.
+ * Keep the SDK's unavailable-helper contract; never fork a root helper or
+ * restore LordOS networking when running alongside NetworkManager. */
+#include "../admin.h"
+#include
+
+int admin_start(const char *user) { (void)user; return 0; }
+int admin_fd(void) { return -1; }
+int admin_known(const char *action) { (void)action; return 0; }
+int admin_send(const char *action, const char *arg, size_t len) {
+ (void)action; (void)arg; (void)len; errno = ENOTSUP; return -1;
+}
+void admin_dispatch(void (*fn)(int, const char *, const char *)) { (void)fn; }
+/* Networking's legacy parsers share some code with its privileged side. These
+ * entrypoints are deliberately unavailable in the standalone binary. */
+void admin_say(const char *kind, const char *fmt, ...) { (void)kind; (void)fmt; }
+void admin_event(const char *fmt, ...) { (void)fmt; }
+int admin_run(const char *const argv[]) { (void)argv; return -1; }
+int admin_run_input(const char *const argv[], const char *input) { (void)argv; (void)input; return -1; }
+int admin_run_capture(const char *const argv[], char *out, size_t n) {
+ (void)argv; if (n) out[0] = 0; return -1;
+}
+int admin_daemon_exec(const char *name, const char *const argv[], const char *const env[]) {
+ (void)name; (void)argv; (void)env; return -1;
+}
+int admin_daemon_fork(const char *name, void (*fn)(void *), void *arg) {
+ (void)name; (void)fn; (void)arg; return -1;
+}
+int admin_daemon_stop(const char *name) { (void)name; return -1; }
+int admin_daemon_running(const char *name) { (void)name; return 0; }
diff --git a/server/src/platform/net.c b/server/src/platform/net.c
new file mode 100644
index 00000000..6bdf8ff3
--- /dev/null
+++ b/server/src/platform/net.c
@@ -0,0 +1,5 @@
+/* Only state.c and nl.c are needed to observe the host network. VPN management
+ * belongs to NetworkManager/the host; do not link LordOS's privileged drivers. */
+#include
+#include
+char *net_vpn_types_json(void) { return strdup("[]"); }
diff --git a/server/src/platform/seat.c b/server/src/platform/seat.c
new file mode 100644
index 00000000..68584587
--- /dev/null
+++ b/server/src/platform/seat.c
@@ -0,0 +1,82 @@
+#include "seat.h"
+#include
+#include
+#include
+
+#ifdef LORD_LIBSEAT
+#include
+#include
+
+static struct libseat *seat;
+static int active;
+static void (*on_enable)(void), (*on_disable)(void);
+static struct { int fd, id; } devices[128];
+static int count;
+
+static void enable(struct libseat *s, void *data) {
+ (void)s; (void)data;
+ active = 1;
+ if (on_enable) on_enable();
+}
+static void disable(struct libseat *s, void *data) {
+ (void)data;
+ active = 0;
+ /* Release GPU resources and grabbed input before acknowledging the switch. */
+ if (on_disable) on_disable();
+ libseat_disable_seat(s);
+}
+static const struct libseat_seat_listener listener = { enable, disable };
+
+int seat_start(void) {
+ libseat_set_log_level(LIBSEAT_LOG_LEVEL_ERROR);
+ seat = libseat_open_seat(&listener, NULL);
+ if (!seat) { perror("lord-de: opening seat (logind or seatd required)"); return -1; }
+ /* Enable may already be queued by open_seat. Dispatch it before opening DRM. */
+ for (int i = 0; !active && i < 5; i++)
+ if (libseat_dispatch(seat, 1000) < 0) break;
+ if (!active) { seat_stop(); errno = EACCES; return -1; }
+ return 0;
+}
+int seat_managed(void) { return seat != NULL; }
+int seat_fd(void) { return seat ? libseat_get_fd(seat) : -1; }
+int seat_dispatch(void) { return seat ? libseat_dispatch(seat, 0) : 0; }
+void seat_callbacks(void (*e)(void), void (*d)(void)) { on_enable = e; on_disable = d; }
+int seat_switch(int vt) { return seat ? libseat_switch_session(seat, vt) : -1; }
+
+int seat_device_open(const char *path, int flags) {
+ if (!seat) return open(path, flags);
+ if (!active || count == 128) { errno = EACCES; return -1; }
+ int fd, id = libseat_open_device(seat, path, &fd);
+ if (id < 0) return -1;
+ fcntl(fd, F_SETFD, FD_CLOEXEC);
+ fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | (flags & O_NONBLOCK));
+ devices[count].fd = fd;
+ devices[count++].id = id;
+ return fd;
+}
+void seat_device_close(int fd) {
+ for (int i = 0; i < count; i++) if (devices[i].fd == fd) {
+ libseat_close_device(seat, devices[i].id);
+ devices[i] = devices[--count];
+ break;
+ }
+ close(fd);
+}
+void seat_stop(void) {
+ on_enable = on_disable = NULL;
+ while (count) seat_device_close(devices[count - 1].fd);
+ if (seat) libseat_close_seat(seat);
+ seat = NULL;
+ active = 0;
+}
+#else
+int seat_start(void) { return 0; }
+void seat_stop(void) { }
+int seat_fd(void) { return -1; }
+int seat_dispatch(void) { return 0; }
+int seat_managed(void) { return 0; }
+int seat_device_open(const char *path, int flags) { return open(path, flags); }
+void seat_device_close(int fd) { close(fd); }
+void seat_callbacks(void (*e)(void), void (*d)(void)) { (void)e; (void)d; }
+int seat_switch(int vt) { (void)vt; return -1; }
+#endif
diff --git a/server/src/platform/seat.h b/server/src/platform/seat.h
new file mode 100644
index 00000000..8eba54a5
--- /dev/null
+++ b/server/src/platform/seat.h
@@ -0,0 +1,16 @@
+#ifndef LORD_SEAT_H
+#define LORD_SEAT_H
+
+/* libseat owns session access; the compositor still owns all graphics/input.
+ * With SEAT=0 these calls retain the original direct-device behavior. */
+int seat_start(void);
+void seat_stop(void);
+int seat_fd(void);
+int seat_dispatch(void);
+int seat_managed(void);
+int seat_device_open(const char *path, int flags);
+void seat_device_close(int fd);
+void seat_callbacks(void (*enable)(void), void (*disable)(void));
+int seat_switch(int vt);
+
+#endif
diff --git a/server/src/term/pty.c b/server/src/term/pty.c
index e0c9a839..ac0dfb1e 100644
--- a/server/src/term/pty.c
+++ b/server/src/term/pty.c
@@ -8,6 +8,7 @@
#include
#include
+#include
#include
#include
#include
@@ -20,7 +21,7 @@
#include
#include
-static char integration[1024]; /* folder with lord-bash.sh, zsh/ and lord-fish.fish (absolute path) */
+static char integration[PATH_MAX]; /* realpath requires room for PATH_MAX bytes */
void pty_set_integration(const char *dir) {
if (!dir || !realpath(dir, integration)) integration[0] = 0;
@@ -75,7 +76,7 @@ static void child(int fd, const char *cwd, int id) {
if (!getenv("LANG")) setenv("LANG", "C.UTF-8", 1);
const char *base = strrchr(shell, '/') ? strrchr(shell, '/') + 1 : shell;
- char rc[1200], argv0[64];
+ char rc[PATH_MAX + 64], argv0[64];
snprintf(argv0, sizeof argv0, "-%s", base); /* "-zsh": login shell */
if (integration[0] && !strcmp(base, "bash")) {
/* interactive bash with integration: the rc emulates a login shell and adds the marks. */
@@ -94,7 +95,7 @@ static void child(int fd, const char *cwd, int id) {
/* fish: --init-command runs after the user's configuration. */
snprintf(rc, sizeof rc, "%s/lord-fish.fish", integration);
if (access(rc, R_OK) == 0) {
- char cmd[1300];
+ char cmd[sizeof rc + 16];
snprintf(cmd, sizeof cmd, "source '%s'", rc);
execl(shell, argv0, "--init-command", cmd, (char *)NULL);
}
diff --git a/server/src/ui/js.c b/server/src/ui/js.c
index a7c080c1..63535761 100644
--- a/server/src/ui/js.c
+++ b/server/src/ui/js.c
@@ -410,7 +410,9 @@ static void session_context(void) {
js_sys_init();
js_sys_register(ctx, lord);
js_term_register(ctx, lord);
+#ifndef LORD_STANDALONE
js_pkg_register(ctx, lord);
+#endif
js_sound_register(ctx, lord);
js_wasm_register(ctx, g); /* the WebAssembly global (our own interpreter, src/wasm) */
js_net_register(ctx, lord);
@@ -425,7 +427,9 @@ static void session_context(void) {
js_input_register(ctx, lord); /* lord.input: touchpads and touchscreens (tp/) */
js_fw_register(ctx, lord); /* lord.fw: per-app network permissions (lord-netperm, docs/FIREWALL.md) */
js_disk_register(ctx, lord); /* lord.disks: disks and partitions (Disks app) */
+#ifndef LORD_STANDALONE
js_imager_register(ctx, lord); /* lord.imager: LordOS Imager (docs/IMAGER.md) */
+#endif
js_backup_register(ctx, lord); /* lord.backup: Backups (backup/) */
js_logs_register(ctx, lord); /* lord.logs: journal and LordOS logs (Logs app) */
js_help_register(ctx, lord); /* lord.help (Help) and lord.time.zoneOffset (Clock, tzif.c) */
diff --git a/server/src/ui/js_sys.c b/server/src/ui/js_sys.c
index 5b7b24ce..3d6bb26b 100644
--- a/server/src/ui/js_sys.c
+++ b/server/src/ui/js_sys.c
@@ -466,8 +466,8 @@ static void read_vars(JSContext *c, JSValue o, const char *path) {
static void apply_locale_conf(void) {
char path[PATH_MAX + 32];
locale_conf(path, sizeof path);
- const char *files[2] = { "/etc/default/locale", path };
- for (int k = 0; k < 2; k++) {
+ const char *files[3] = { "/etc/default/locale", "/etc/locale.conf", path };
+ for (int k = 0; k < 3; k++) {
char *txt = read_small(files[k], 8192, NULL);
for (char *line = txt ? strtok(txt, "\n") : NULL; line; line = strtok(NULL, "\n")) {
char *eq = strchr(line, '=');
@@ -489,6 +489,7 @@ static JSValue f_locale_get(JSContext *c, JSValueConst this_val, int argc, JSVal
locale_conf(path, sizeof path);
read_vars(c, u, path);
read_vars(c, sys, "/etc/default/locale");
+ read_vars(c, sys, "/etc/locale.conf");
JS_SetPropertyStr(c, o, "user", u);
JS_SetPropertyStr(c, o, "system", sys);
return o;
@@ -528,11 +529,18 @@ static JSValue f_locale_available(JSContext *c, JSValueConst this_val, int argc,
(void)this_val; (void)argc; (void)argv;
JSValue arr = JS_NewArray(c);
FILE *f = fopen("/usr/share/i18n/SUPPORTED", "r");
+ /* Fedora ships the locale archive without Debian's SUPPORTED catalog.
+ * Enumerate installed locales with a fixed, unprivileged command there. */
+ int installed_only = f == NULL;
+ if (installed_only) f = popen("/usr/bin/locale -a", "re");
char line[256];
uint32_t i = 0;
while (f && fgets(line, sizeof line, f)) {
char loc[128], cs[64];
- if (sscanf(line, "%127s %63s", loc, cs) != 2 || strcmp(cs, "UTF-8")) continue;
+ if (installed_only) {
+ if (sscanf(line, "%127s", loc) != 1) continue;
+ if (!strcasestr(loc, "utf8") && !strcasestr(loc, "utf-8")) continue;
+ } else if (sscanf(line, "%127s %63s", loc, cs) != 2 || strcmp(cs, "UTF-8")) continue;
locale_t l = newlocale(LC_ALL_MASK, loc, (locale_t)0);
if (l) freelocale(l);
JSValue o = JS_NewObject(c);
@@ -540,7 +548,7 @@ static JSValue f_locale_available(JSContext *c, JSValueConst this_val, int argc,
JS_SetPropertyStr(c, o, "generated", JS_NewBool(c, l != 0));
JS_SetPropertyUint32(c, arr, i++, o);
}
- if (f) fclose(f);
+ if (f) { if (installed_only) pclose(f); else fclose(f); }
return arr;
}
diff --git a/server/tests/platform_admin_test.c b/server/tests/platform_admin_test.c
new file mode 100644
index 00000000..7b99091f
--- /dev/null
+++ b/server/tests/platform_admin_test.c
@@ -0,0 +1,19 @@
+#include "admin.h"
+#include
+#include
+
+int main(void) {
+ assert(admin_start("root") == 0);
+ assert(admin_fd() == -1);
+ static const char *const actions[] = {
+ "pkg-install", "pkg-update", "net-restore", "net-manage", "install-run",
+ "locale-gen", "langpack", "ssh-set", "users-create", "disk-format"
+ };
+ for (unsigned i = 0; i < sizeof actions / sizeof *actions; i++) {
+ assert(!admin_known(actions[i]));
+ assert(admin_send(actions[i], "", 0) == -1);
+ }
+ const char *const cmd[] = { "/bin/true", NULL };
+ assert(admin_run(cmd) == -1);
+ puts("standalone: no privileged distribution actions");
+}
diff --git a/server/tests/seat_test.c b/server/tests/seat_test.c
new file mode 100644
index 00000000..3efcfe14
--- /dev/null
+++ b/server/tests/seat_test.c
@@ -0,0 +1,72 @@
+/* Simulate logind/seatd revocation without taking over a real seat. */
+#include "platform/seat.h"
+#include
+#include
+#include
+#include
+#include
+#include
+
+struct libseat { int unused; };
+static struct libseat fake;
+static const struct libseat_seat_listener *listener;
+static void *userdata;
+static int event, opens, releases, ack, enabled, disabled;
+static int device = -1;
+void libseat_set_log_level(enum libseat_log_level level) { (void)level; }
+struct libseat *libseat_open_seat(const struct libseat_seat_listener *l, void *d) {
+ listener = l; userdata = d; event = 1; return &fake;
+}
+int libseat_close_seat(struct libseat *s) { assert(s == &fake); return 0; }
+int libseat_get_fd(struct libseat *s) { (void)s; return 42; }
+int libseat_dispatch(struct libseat *s, int timeout) {
+ (void)timeout;
+ int e = event; event = 0;
+ if (e == 1) listener->enable_seat(s, userdata);
+ if (e == 2) listener->disable_seat(s, userdata);
+ return e != 0;
+}
+int libseat_disable_seat(struct libseat *s) {
+ (void)s;
+ assert(disabled && device == -1); /* caller released devices before ack */
+ ack++;
+ return 0;
+}
+int libseat_open_device(struct libseat *s, const char *path, int *fd) {
+ (void)s; (void)path;
+ *fd = open("/dev/null", O_RDONLY);
+ assert(*fd >= 0);
+ opens++;
+ return *fd + 100;
+}
+int libseat_close_device(struct libseat *s, int id) {
+ (void)s;
+ assert(fcntl(id - 100, F_GETFD) >= 0); /* libseat requires the fd still open */
+ releases++;
+ return 0;
+}
+int libseat_switch_session(struct libseat *s, int n) { (void)s; return n == 3 ? 0 : -1; }
+static void enable(void) { enabled++; device = seat_device_open("/dev/input/event0", O_RDONLY | O_NONBLOCK); assert(device >= 0); }
+static void disable(void) { disabled++; seat_device_close(device); device = -1; }
+
+int main(void) {
+ assert(!seat_managed());
+ assert(seat_start() == 0 && seat_managed());
+ assert(seat_fd() == 42);
+ seat_callbacks(enable, disable);
+ enable();
+ assert(fcntl(device, F_GETFD) & FD_CLOEXEC);
+ assert(fcntl(device, F_GETFL) & O_NONBLOCK);
+ event = 2;
+ seat_dispatch();
+ assert(ack == 1 && opens == releases);
+ assert(seat_device_open("/dev/dri/card0", O_RDWR) == -1 && errno == EACCES);
+ event = 1;
+ seat_dispatch();
+ assert(enabled == 2 && device >= 0);
+ assert(seat_switch(3) == 0);
+ seat_stop();
+ assert(opens == releases && !seat_managed() && seat_fd() == -1);
+ assert(fcntl(device, F_GETFD) == -1);
+ puts("seat: devices released before disable, reopened on enable, closed on exit");
+}
diff --git a/tools/build-theme.mjs b/tools/build-theme.mjs
index 3efd3e67..18567e26 100644
--- a/tools/build-theme.mjs
+++ b/tools/build-theme.mjs
@@ -8,6 +8,30 @@ import path from 'node:path';
import { writeManifest } from './code-manifest.mjs';
const root = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
+const profile = process.env.LORD_PROFILE || 'desktop';
+if (!['desktop', 'os'].includes(profile)) throw new Error('LORD_PROFILE must be desktop or os');
+const standalone = profile === 'desktop';
+const profileFile = path.join(root, 'build-profile.json');
+const previous = fs.existsSync(profileFile) ? JSON.parse(fs.readFileSync(profileFile, 'utf8')) : null;
+if (previous !== profile) {
+ // A bundle from another profile must never leak into the new code manifest.
+ for (const name of fs.readdirSync(path.join(root, 'themes'))) {
+ if (!fs.statSync(path.join(root, 'themes', name)).isDirectory()) continue;
+ fs.rmSync(path.join(root, 'themes', name, 'theme.bundle.js'), { force: true });
+ }
+ fs.rmSync(path.join(root, 'assets', 'sysapps'), { recursive: true, force: true });
+ fs.rmSync(path.join(root, 'assets', 'app-host.bundle.js'), { force: true });
+}
+const defines = { 'process.env.NODE_ENV': '"production"', __LORD_STANDALONE__: String(standalone) };
+const includeApp = (id) => !standalone || !['imager', 'installer'].includes(id);
+const hostStorePlugin = {
+ name: 'lord-de-host-store',
+ setup(b) {
+ if (standalone) b.onResolve({ filter: /^\.\/debian\.js$/ }, (a) =>
+ a.importer === path.join(root, 'apps/store/index.jsx')
+ ? { path: path.join(root, 'apps/store/host.jsx') } : null);
+ },
+};
// Virtual module '@lordos/apps': one entry per apps//manifest.json.
const appsPlugin = {
@@ -16,7 +40,7 @@ const appsPlugin = {
b.onResolve({ filter: /^@lordos\/apps$/ }, () => ({ path: 'apps', namespace: 'lordos-apps' }));
b.onLoad({ filter: /.*/, namespace: 'lordos-apps' }, () => {
const dir = path.join(root, 'apps');
- const ids = fs.readdirSync(dir).filter((d) => fs.existsSync(path.join(dir, d, 'manifest.json'))).sort();
+ const ids = fs.readdirSync(dir).filter((d) => includeApp(d) && fs.existsSync(path.join(dir, d, 'manifest.json'))).sort();
const lines = ids.map((id, i) => {
const base = path.join(dir, id);
return `import * as A${i} from ${JSON.stringify(path.join(base, 'index.jsx'))};\n` +
@@ -51,8 +75,8 @@ async function buildTheme(name) {
'@lordos/runtime': path.join(root, 'runtime', 'index.js'),
'@lordos/sdk': path.join(root, 'sdk', 'index.js'),
},
- plugins: [appsPlugin],
- define: { 'process.env.NODE_ENV': '"production"' },
+ plugins: [hostStorePlugin, appsPlugin],
+ define: defines,
metafile: true,
logLevel: 'warning',
});
@@ -70,7 +94,7 @@ async function buildAppHost() {
outfile: path.join(root, 'assets', 'app-host.bundle.js'),
bundle: true, format: 'iife', target: 'es2020', minify: true, jsx: 'automatic',
alias: { '@lordos/runtime': path.join(root, 'runtime', 'index.js'), '@lordos/sdk': path.join(root, 'sdk', 'index.js') },
- define: { 'process.env.NODE_ENV': '"production"' }, metafile: true, logLevel: 'warning',
+ define: defines, metafile: true, logLevel: 'warning',
});
const bytes = Object.values(result.metafile.outputs)[0].bytes;
console.log(`app-host: ${(bytes / 1024).toFixed(0)} KB in ${Date.now() - t0} ms`);
@@ -91,6 +115,7 @@ async function buildSysApps() {
},
};
for (const id of fs.readdirSync(dir).sort()) {
+ if (!includeApp(id)) continue;
const mf = path.join(dir, id, 'manifest.json');
if (!fs.existsSync(mf)) continue;
const manifest = JSON.parse(fs.readFileSync(mf, 'utf8'));
@@ -101,7 +126,7 @@ async function buildSysApps() {
const result = await build({
entryPoints: [path.join(dir, id, 'index.jsx')],
bundle: true, write: false, format: 'iife', globalName: '__lordosApp', target: 'es2020', minify: true,
- jsx: 'automatic', plugins: [sharedPlugin], define: { 'process.env.NODE_ENV': '"production"' }, logLevel: 'warning',
+ jsx: 'automatic', plugins: [sharedPlugin], define: defines, logLevel: 'warning',
});
fs.writeFileSync(path.join(out, 'app.js'), result.outputFiles[0].contents);
fs.writeFileSync(path.join(out, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`);
@@ -117,11 +142,12 @@ async function buildSysApps() {
}
const all = fs.readdirSync(path.join(root, 'themes'))
- .filter((d) => fs.existsSync(path.join(root, 'themes', d, 'index.jsx')));
+ .filter((d) => (!standalone || d !== 'greeter') && fs.existsSync(path.join(root, 'themes', d, 'index.jsx')));
for (const name of (names.length ? names : all).filter((n) => n !== 'app-host' && n !== 'sysapps')) await buildTheme(name);
if (!names.length || names.includes('app-host')) await buildAppHost();
if (!names.length || names.includes('sysapps')) await buildSysApps();
// the verified-code manifest of what was just built (unsigned: a development build until
// tools/release/sign-code.sh signs it on the maintainer's machine)
const cm = writeManifest(root);
+fs.writeFileSync(profileFile, `${JSON.stringify(profile)}\n`);
console.log(`code-manifest.json: ${cm.files} files${cm.changed ? '' : ' (unchanged)'}`);
diff --git a/tools/desktop/check.py b/tools/desktop/check.py
new file mode 100644
index 00000000..065b44c0
--- /dev/null
+++ b/tools/desktop/check.py
@@ -0,0 +1,76 @@
+#!/usr/bin/env python3
+"""Check that built/staged artifacts form an independent desktop."""
+import hashlib
+import json
+import os
+from pathlib import Path
+import subprocess
+import tempfile
+
+from install import install, REPO
+
+
+def run(*args, **kwargs):
+ return subprocess.run(args, check=True, text=True, capture_output=True, **kwargs).stdout
+
+
+def main():
+ assert json.loads((REPO / 'build-profile.json').read_text()) == 'desktop'
+ symbols = run('nm', str(REPO / 'server/build/lord-server'))
+ for symbol in ['pkg_act_install', 'pkg_act_update', 'install_act_run', 'net_act_restore',
+ 'act_langpack', 'js_pkg_register', 'js_imager_register']:
+ assert symbol not in symbols, f'OS component linked: {symbol}'
+ assert 'libseat_open_seat' in symbols, 'desktop must use a host seat backend'
+ manifest = json.loads((REPO / 'code-manifest.json').read_text())
+ for file in manifest['files']:
+ path = REPO / file['path']
+ assert path.stat().st_size == file['size']
+ assert hashlib.sha256(path.read_bytes()).hexdigest() == file['sha256']
+ assert not file['path'].startswith('themes/greeter/')
+ for name in ['default', 'classic']:
+ bundle = (REPO / f'themes/{name}/theme.bundle.js').read_text()
+ assert 'pkg-install-file' not in bundle, 'Debian store bundled into desktop'
+ assert 'install-run' not in bundle, 'OS installer bundled into desktop'
+ with tempfile.TemporaryDirectory(prefix='lord-de-stage-') as tmp:
+ install('/usr', '/usr/libexec', tmp)
+ staged = Path(tmp) / 'usr/libexec/lord-de'
+ assert set(p.name for p in (staged / 'bin').iterdir()) <= {
+ 'lord-server', 'lord-vault', 'lord-maild', 'lord-doc', 'lord-media'}
+ assert not (Path(tmp) / 'etc').exists()
+ assert not (Path(tmp) / 'usr/lib/systemd').exists()
+ assert (Path(tmp) / 'usr/share/wayland-sessions/lord-de.desktop').is_file()
+ assert 'FAILED' not in run(str(staged / 'bin/lord-server'), '--check-code', str(staged))
+ Path(tmp).chmod(0o755)
+ # An ordinary user can initialize the complete desktop without root or a seat
+ # in headless mode. This does not validate DRM/KMS or physical input.
+ with tempfile.TemporaryDirectory(prefix='lord-de-home-') as tmp:
+ home = Path(tmp)
+ (home / 'runtime').mkdir(mode=0o700)
+ env = dict(os.environ, HOME=tmp, USER='nobody', XDG_RUNTIME_DIR=str(home / 'runtime'))
+ for key in ['SUDO_USER', 'DBUS_SESSION_BUS_ADDRESS', 'WAYLAND_DISPLAY', 'DISPLAY']:
+ env.pop(key, None)
+ opts = {}
+ if os.geteuid() == 0:
+ os.chown(home, 65534, 65534)
+ os.chown(home / 'runtime', 65534, 65534)
+ opts = {'user': 65534, 'group': 65534, 'extra_groups': []}
+ for theme in ['default', 'classic']:
+ try:
+ run(str(staged / 'bin/lord-server'), '--headless', '800x600', '--timeout', '1',
+ '--theme', f'../themes/{theme}/theme.bundle.js',
+ cwd=staged / 'bin', env=env, timeout=15, **opts)
+ except subprocess.CalledProcessError as error:
+ log = home / '.cache/lordos/session.log'
+ print(error.stderr)
+ if log.exists():
+ print(log.read_text())
+ raise
+ log = (home / '.cache/lordos/session.log').read_text()
+ assert f'js: theme ../themes/{theme}/theme.bundle.js loaded' in log, log
+ for error in ['ReferenceError:', 'TypeError:', 'SyntaxError:', 'code refused']:
+ assert error not in log, log
+ print('desktop: no OS backend, manifest valid, installation isolated, headless startup passed')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/desktop/dist.sh b/tools/desktop/dist.sh
new file mode 100644
index 00000000..55c323d3
--- /dev/null
+++ b/tools/desktop/dist.sh
@@ -0,0 +1,33 @@
+#!/bin/sh
+# Prepare a local RPM source archive with the frontend built from the lockfile.
+set -eu
+cd "$(dirname "$0")/../.."
+npm ci
+LORD_PROFILE=desktop npm run build
+python3 - <<'PY'
+import json
+from pathlib import Path
+import tarfile
+
+root = Path.cwd()
+version = json.loads((root / 'package.json').read_text())['version']
+name = f'lord-de-{version}'
+out = root / 'dist' / f'{name}.tar.gz'
+out.parent.mkdir(exist_ok=True)
+exclude = {'.git', 'node_modules', 'build', 'dist', '__pycache__', 'target'}
+
+def select(info):
+ if any(p in exclude for p in Path(info.name).parts[1:]):
+ return None
+ if info.name.endswith(('.rpm', '.deb', '.iso', '.img', '.lpk', '.pyc')):
+ return None
+ return info
+
+with tarfile.open(out, 'w:gz') as archive:
+ for path in ['server', 'runtime', 'sdk', 'apps', 'themes', 'assets', 'third_party',
+ 'tests', 'tools/desktop', 'tools/build-theme.mjs', 'tools/code-manifest.mjs', 'packaging/desktop',
+ 'packaging/rpm', 'Makefile', 'package.json', 'package-lock.json',
+ 'code-manifest.json', 'build-profile.json', 'LICENSE', 'THIRD_PARTY.md', 'README.md', 'docs/STANDALONE.md']:
+ archive.add(root / path, arcname=f'{name}/{path}', filter=select)
+print(out)
+PY
diff --git a/tools/desktop/install.py b/tools/desktop/install.py
new file mode 100644
index 00000000..c7d87e3e
--- /dev/null
+++ b/tools/desktop/install.py
@@ -0,0 +1,89 @@
+#!/usr/bin/env python3
+"""Stage the desktop only. No host configuration or services are changed."""
+import argparse
+import json
+import hashlib
+from pathlib import Path
+import shutil
+
+REPO = Path(__file__).resolve().parents[2]
+
+
+def install(prefix, libexecdir, destdir):
+ prefix, libexecdir = Path(prefix), Path(libexecdir)
+ if not prefix.is_absolute() or not libexecdir.is_absolute():
+ raise ValueError('prefix and libexecdir must be absolute paths')
+ if any(c.isspace() or c in "'\"\\" for c in str(prefix) + str(libexecdir)):
+ raise ValueError('installation paths must not contain whitespace or shell quotes')
+ root = libexecdir / 'lord-de'
+ stage = Path(destdir) if destdir else Path('/')
+ target = stage / root.relative_to('/')
+ required = ['server/build/lord-server', 'server/build/lord-vault',
+ 'themes/default/theme.bundle.js', 'themes/classic/theme.bundle.js',
+ 'assets/app-host.bundle.js', 'code-manifest.json', 'server/build/gen/profile.h']
+ for rel in required:
+ if not (REPO / rel).is_file():
+ raise ValueError(f'missing {rel}; run npm ci && make first')
+ profile = (REPO / 'server/build/gen/profile.h').read_text()
+ if 'profile=desktop' not in profile:
+ raise ValueError('refusing to install a legacy OS build as Lord DE')
+ if (REPO / 'build-profile.json').read_text().strip() != '"desktop"':
+ raise ValueError('themes must be built with LORD_PROFILE=desktop')
+ # Validate the complete install plan before copying any file.
+ manifest = json.loads((REPO / 'code-manifest.json').read_text())
+ code_paths = {f['path'] for f in manifest['files']}
+ if any(p.startswith('themes/greeter/') for p in code_paths):
+ raise ValueError('stale OS bundles found; rebuild the desktop themes')
+ for file in manifest['files']:
+ path = REPO / file['path']
+ if not path.is_file() or hashlib.sha256(path.read_bytes()).hexdigest() != file['sha256']:
+ raise ValueError(f"stale or missing bundle: {file['path']}; rebuild the themes")
+
+ def copy(src, dst, mode=None):
+ dst.parent.mkdir(parents=True, exist_ok=True)
+ shutil.copy2(src, dst)
+ if mode is not None:
+ dst.chmod(mode)
+
+ for name in ['lord-server', 'lord-vault', 'lord-maild', 'lord-doc', 'lord-media']:
+ src = REPO / 'server/build' / name
+ if src.is_file():
+ copy(src, target / 'bin' / name, 0o755)
+ for name in ['fonts', 'locales', 'shell', 'icons', 'mascot', 'brand', 'monitor', 'help', 'wallpapers', 'gtk', 'store']:
+ shutil.copytree(REPO / 'assets' / name, target / 'assets' / name, dirs_exist_ok=True)
+ copy(REPO / 'assets/app-host.bundle.js', target / 'assets/app-host.bundle.js')
+ shutil.copytree(REPO / 'assets/sysapps', target / 'assets/sysapps', dirs_exist_ok=True)
+ theme_names = sorted({p.split('/')[1] for p in code_paths if p.startswith('themes/')})
+ for name in theme_names:
+ for file in ['theme.json', 'theme.bundle.js']:
+ copy(REPO / 'themes' / name / file, target / 'themes' / name / file)
+ assets = REPO / 'themes' / name / 'assets'
+ if assets.exists():
+ shutil.copytree(assets, target / 'themes' / name / 'assets', dirs_exist_ok=True)
+ copy(REPO / 'code-manifest.json', target / 'code-manifest.json')
+ if (REPO / 'code-manifest.sig').exists():
+ copy(REPO / 'code-manifest.sig', target / 'code-manifest.sig')
+ bindir = prefix / 'bin'
+ launcher = stage / bindir.relative_to('/') / 'lord-de-session'
+ launcher.parent.mkdir(parents=True, exist_ok=True)
+ launcher.write_text((REPO / 'tools/desktop/lord-de-session.in').read_text().replace('@ROOT@', str(root)))
+ launcher.chmod(0o755)
+ session = stage / prefix.relative_to('/') / 'share/wayland-sessions/lord-de.desktop'
+ session.parent.mkdir(parents=True, exist_ok=True)
+ session.write_text((REPO / 'packaging/desktop/lord-de.desktop.in').read_text().replace('@BINDIR@', str(bindir)))
+ for file in ['LICENSE', 'THIRD_PARTY.md', 'README.md']:
+ copy(REPO / file, stage / prefix.relative_to('/') / 'share/doc/lord-de' / file)
+ copy(REPO / 'docs/STANDALONE.md', stage / prefix.relative_to('/') / 'share/doc/lord-de/docs/STANDALONE.md')
+ for file in ['LICENSE', 'LICENSE-SecLists']:
+ for src in (REPO / 'third_party').rglob(file):
+ copy(src, stage / prefix.relative_to('/') / 'share/licenses/lord-de/third_party' / src.relative_to(REPO / 'third_party'))
+ print(f'Installed desktop in {target}')
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--prefix', default='/usr/local')
+ parser.add_argument('--libexecdir', default='/usr/local/libexec')
+ parser.add_argument('--destdir', default='')
+ args = parser.parse_args()
+ install(args.prefix, args.libexecdir, args.destdir)
diff --git a/tools/desktop/lord-de-session.in b/tools/desktop/lord-de-session.in
new file mode 100644
index 00000000..7873c55a
--- /dev/null
+++ b/tools/desktop/lord-de-session.in
@@ -0,0 +1,19 @@
+#!/bin/sh
+set -eu
+if [ "$(id -u)" = 0 ]; then
+ echo 'Start Lord DE as your normal session user, without sudo.' >&2
+ exit 1
+fi
+export XDG_SESSION_TYPE=wayland
+export XDG_SESSION_DESKTOP=lord-de
+export XDG_CURRENT_DESKTOP=LordOS
+export DESKTOP_SESSION=lord-de
+if [ -z "${XDG_RUNTIME_DIR:-}" ] || [ ! -d "$XDG_RUNTIME_DIR" ]; then
+ echo 'A login session with XDG_RUNTIME_DIR is required (logind or seatd).' >&2
+ exit 1
+fi
+cd '@ROOT@/bin'
+if [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ]; then
+ exec dbus-run-session -- ./lord-server "$@"
+fi
+exec ./lord-server "$@"