Compare commits

...
9 Commits
Author SHA1 Message Date
dasandClaude Opus 5.5 acb7532a4f Stage 7: command-line helpers dsv, duser, dfw, dsys (0.5.0)
- das-helpers replaces the dsv package and adds shared functions in
  /usr/lib/das/helpers.sh (bilingual messages, sudo re-exec, prompts).
- dsv new creates a runit service (command, user, directory, environment,
  svlogd log) and checks it stays up; dsv rm deletes one not from a package.
- duser: list, add (installer groups, --admin, key, password), del, passwd,
  lock/unlock (key logins too), key add|list|rm, groups +/-.
- dfw: allow/deny/status/reload for the tcp_ports and udp_ports sets of the
  inet das table, kept in /etc/nftables.d/dfw.nft and checked with nft -c.
- dsys: info, hostname, timezone, locale, keymap.
- xtools-minimal on both images.
- nftables.conf no longer suggests restarting the nftables service: its
  finish script flushes the whole ruleset, fail2ban's and Docker's included.
- das-server-install 0.2.0 mentions the helpers when it finishes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 06:58:25 +03:00
dasandClaude Opus 5.5 84768962fc Shrink the ISOs: no GPU drivers in the live initrd, server without GPU and sound firmware
- build-iso.sh patches the container's mklive copy: dracut omits amdgpu,
  radeon, nouveau, i915 and xe (and so their firmware) from the live initrd,
  mksquashfs uses 1 MB blocks with the x86 BCJ filter. initrd 245 -> 89 MB.
- das-server 0.2.0 ships /etc/xbps.d/das-server.conf that ignores
  linux-firmware-nvidia, sof-firmware and alsa-firmware; the server ISO
  passes the same list to mklive -g.
- das-install-common 0.2.0: postinstall drops mklive-ignore.conf.

Desktop ISO 1.96 -> 1.72 GB, server ISO 1.57 -> 1.14 GB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 06:58:25 +03:00
dasandClaude Opus 5.5 eba4516998 Stage 6: server image with a console installer (0.4.0)
- server/ profile and build-iso.sh [desktop|server]; make iso-server,
  make qemu-server, run-qemu.sh --serial; login on tty and ttyS0
- das-server: SSH, chrony, NetworkManager/dhcpcd, Docker with Compose,
  nftables (closed except SSH, own table), fail2ban reading socklog,
  sshd without root passwords, Docker log rotation, tools
- das-server-install: dialog installer in Ukrainian and English, or
  unattended with --config; GPT + Btrfs subvolumes, GRUB for UEFI and BIOS,
  serial console, SSH key with optional keys-only logins
- das-install-common: mount-source and postinstall shared with Calamares
- dsv: runit service manager (list, enable, disable, start, stop,
  restart, status, logs) with bash completion, also on the desktop
- das-update split into a subpackage without GTK; the window and tray
  icon move to /usr/libexec/das/das-update-gui
- das-snapper-setup enables dbus, which snapper needs
- TODO: ISO size review with measured numbers and options

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 21:28:26 +03:00
dasandClaude Opus 5.5 5349a028c8 Fix issues found testing stage 5 in a VM
- Calamares branding takes the version from VERSION at ISO build time
- das-update tray hides its icon when updates were installed elsewhere
  (watches the xbps package database)
- Installer removes guest tools of other hypervisors; the live image
  starts VBoxClient only inside VirtualBox
- Live session no longer locks the screen while the installer runs
- das-tools 0.2.0, das-installer 0.2.0, das-settings 0.4.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 19:43:51 +03:00
dasandClaude Opus 5.5 d1d76c2bef Stage 5: das-update and das-welcome (0.3.0)
das-tools (Go, GTK 3 via gotk3):
- das-update: update window, tray icon when updates are available,
  --cli mode; xbps-install -Su between a snapper pre/post snapshot pair,
  privileged part through pkexec, rollback and reboot hints
- das-welcome: first-login window with install (live), language,
  home folder renaming, updates, settings and help

das-settings 0.3.0: create home folders with translated names.
xdg-user-dirs 0.20 only looks for translations in the first
XDG_DATA_DIRS entry with a locale directory (/usr/local/share/locale).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 18:24:39 +03:00
dasandClaude Opus 5.5 3b4527990e Rename planned das-sv to dsv
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 17:21:31 +03:00
dasandClaude Opus 5.5 6d53240b75 Plan the server image in TODO
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 17:03:34 +03:00
dasandClaude Opus 5.5 5d478822ac Add TODO with the remaining work
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 09:17:09 +03:00
dasandClaude Opus 5.5 ecced4e3f4 Stage 4: Calamares installer and Btrfs snapshots (0.2.0)
- Package Calamares 3.4.3 (Qt6, widgets UI) with xbps-src.
- das-installer: Calamares setup for Void/runit with Das OS branding and
  slides. It copies the clean live image (live-base), builds locales and the
  host-only initramfs, rewrites kernels without sparse tails (GRUB 2.12 cannot
  read Btrfs holes at the end of a file), then removes itself.
- das-snapshots: flat Btrfs layout (@, @home, @snapshots, @var_log,
  @var_cache_xbps), snapper with hourly snapshots via cronie, grub-btrfs menu,
  snapper-rollback, and a dracut module that boots read-only snapshots with
  a RAM overlay.
- das-settings 0.2.0: start LightDM with the system locale.
- Live image: GRUB, efibootmgr and the new packages; Das OS /etc/issue.
- Build: wipe stale destdirs before building packages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 08:59:09 +03:00
120 changed files with 6143 additions and 56 deletions
+10 -1
View File
@@ -1,13 +1,15 @@
# Das OS build entry points. Everything except QEMU runs in the Void builder container. # Das OS build entry points. Everything except QEMU runs in the Void builder container.
.PHONY: help packages iso qemu qemu-bios qemu-disk test clean .PHONY: help packages iso iso-server qemu qemu-bios qemu-disk qemu-server test clean
help: help:
@echo "make packages build srcpkgs/* into the local repository (out/hostdir/binpkgs)" @echo "make packages build srcpkgs/* into the local repository (out/hostdir/binpkgs)"
@echo "make iso build packages, then the live ISO (out/iso)" @echo "make iso build packages, then the live ISO (out/iso)"
@echo "make iso-server build packages, then the server ISO (out/iso)"
@echo "make qemu boot the newest ISO in QEMU with UEFI" @echo "make qemu boot the newest ISO in QEMU with UEFI"
@echo "make qemu-bios boot the newest ISO in QEMU with legacy BIOS" @echo "make qemu-bios boot the newest ISO in QEMU with legacy BIOS"
@echo "make qemu-disk boot with a 20 GB virtual disk attached (for installer tests)" @echo "make qemu-disk boot with a 20 GB virtual disk attached (for installer tests)"
@echo "make qemu-server boot the newest server ISO on a serial console, with the disk"
@echo "make test run Go tests" @echo "make test run Go tests"
@echo "make clean remove build output (keeps download caches)" @echo "make clean remove build output (keeps download caches)"
@@ -17,6 +19,9 @@ packages:
iso: packages iso: packages
scripts/builder.sh /work/scripts/container/build-iso.sh scripts/builder.sh /work/scripts/container/build-iso.sh
iso-server: packages
scripts/builder.sh /work/scripts/container/build-iso.sh server
qemu: qemu:
scripts/run-qemu.sh scripts/run-qemu.sh
@@ -26,8 +31,12 @@ qemu-bios:
qemu-disk: qemu-disk:
scripts/run-qemu.sh --disk scripts/run-qemu.sh --disk
qemu-server:
scripts/run-qemu.sh --serial --disk
test: test:
cd srcpkgs/das-theme/files/xfwm4gen && go test ./... cd srcpkgs/das-theme/files/xfwm4gen && go test ./...
cd srcpkgs/das-tools/files/src && go test ./...
# Build output may be owned by root when a build was interrupted, so remove it from the container # Build output may be owned by root when a build was interrupted, so remove it from the container
clean: clean:
+102 -9
View File
@@ -1,15 +1,21 @@
## Das OS ## Das OS
### _A light, friendly Void Linux–based desktop with Xfce and a Ukrainian touch_ ### _A light, friendly Void Linux–based desktop with Xfce and a Ukrainian touch, plus a console server edition_
[![Void Linux](https://img.shields.io/badge/Void_Linux-x86__64_glibc-green)](https://voidlinux.org) [![Xfce](https://img.shields.io/badge/Xfce-4.20-orange)](https://xfce.org) [![runit](https://img.shields.io/badge/init-runit-blue)](https://docs.voidlinux.org/config/services/) [![Go](https://img.shields.io/badge/Go-1.22+-blue)](https://go.dev) [![Version](https://img.shields.io/badge/version-0.1.0-lightgrey)](#) [![Void Linux](https://img.shields.io/badge/Void_Linux-x86__64_glibc-green)](https://voidlinux.org) [![Xfce](https://img.shields.io/badge/Xfce-4.20-orange)](https://xfce.org) [![runit](https://img.shields.io/badge/init-runit-blue)](https://docs.voidlinux.org/config/services/) [![Go](https://img.shields.io/badge/Go-1.24+-blue)](https://go.dev) [![Docker](https://img.shields.io/badge/Docker-29-blue)](https://www.docker.com) [![Version](https://img.shields.io/badge/version-0.5.0-lightgrey)](#)
🇺🇦 [Українська версія](README_ua.md) 🇺🇦 [Українська версія](README_ua.md)
### 💡 Purpose ### 💡 Purpose
Void Linux is fast, simple and independent: runit instead of systemd, and its own xbps package manager. Out of the box, though, it is a bare system, and getting it ready for daily use takes hours of setup. Das OS builds a live ISO that boots straight into a configured Xfce desktop. It has its own look, a Ukrainian interface and keyboard layout, and the usual desktop apps. Everything Das OS adds comes from its own xbps packages, so the setup can be reproduced and updated like any other package. Void Linux is fast, simple and independent: runit instead of systemd, and its own xbps package manager. Out of the box, though, it is a bare system, and getting it ready for daily use takes hours of setup. Das OS builds a live ISO that boots straight into a configured Xfce desktop and installs it with a graphical installer. It has its own look, a Ukrainian interface and keyboard layout, the usual desktop apps and Btrfs snapshots that let a broken update be undone. Everything Das OS adds comes from its own xbps packages, so the setup can be reproduced and updated like any other package. A second, console-only ISO installs a server with SSH, a firewall, fail2ban and Docker set up, on the same Btrfs layout with snapshots.
### 📃 Features ### 📃 Features
- [Live ISO](#LiveISO) that boots on UEFI and BIOS straight into the desktop - [Live ISO](#LiveISO) that boots on UEFI and BIOS straight into the desktop
- [Graphical installer](#Installer) (Calamares) set up for Void and runit
- [Snapshots and rollback](#Snapshots): hourly Btrfs snapshots, bootable from the GRUB menu
- [System update](#Update) with a snapshot taken before it: a window, a tray icon and a terminal mode
- [Welcome window](#Welcome) at the first login: language, home folders, updates and help
- [Server image](#Server): a console live ISO with a text installer, SSH, firewall, fail2ban and Docker
- [Command-line helpers](#Helpers) for everyday admin tasks: services (`dsv`), users (`duser`), firewall (`dfw`) and system settings (`dsys`)
- [Das OS look](#Look): light and dark themes, generated window frames, wallpapers and logo - [Das OS look](#Look): light and dark themes, generated window frames, wallpapers and logo
- [Desktop defaults](#Defaults) for Xfce that don't overwrite stock files - [Desktop defaults](#Defaults) for Xfce that don't overwrite stock files
- [Own package repository](#Repository) built by xbps-src from in-tree templates - [Own package repository](#Repository) built by xbps-src from in-tree templates
@@ -17,9 +23,11 @@ Void Linux is fast, simple and independent: runit instead of systemd, and its ow
### ⭐ Advantages ### ⭐ Advantages
- **Void underneath** – a fast runit boot, the xbps package manager and fresh software, with none of the manual setup. - **Void underneath** – a fast runit boot, the xbps package manager and fresh software, with none of the manual setup.
- **Safe updates** – `das-update` takes a snapshot before every update, the system can return to any snapshot, and personal files in `/home` are never part of a rollback.
- **Light** – no systemd, plus Xfce, LightDM and PipeWire, so it runs well on older laptops too. - **Light** – no systemd, plus Xfce, LightDM and PipeWire, so it runs well on older laptops too.
- **Nothing hidden** – all customisations ship as ordinary xbps packages, so `xbps-query -f das-settings` shows exactly what changed. - **Nothing hidden** – all customisations ship as ordinary xbps packages, so `xbps-query -f das-settings` shows exactly what changed.
- **Users keep control** – defaults live in `/etc/xdg/das`, so a user's own settings always take precedence and upgrades never reset them. - **Users keep control** – defaults live in `/etc/xdg/das`, so a user's own settings always take precedence and upgrades never reset them.
- **One base for desktop and server** – both images share the packages, the disk layout, snapshots and `das-update`, so a server is updated and rolled back the same way as a laptop.
- **Builds anywhere** – the build runs in a Void container, so the host can be Mint, Ubuntu or Fedora. - **Builds anywhere** – the build runs in a Void container, so the host can be Mint, Ubuntu or Fedora.
### 📱 Screenshots ### 📱 Screenshots
@@ -44,6 +52,26 @@ Void Linux is fast, simple and independent: runit instead of systemd, and its ow
<td align="center"><i>Dark wallpaper</i></td> <td align="center"><i>Dark wallpaper</i></td>
<td align="center"><i>Window frames: light and dark, active and inactive</i></td> <td align="center"><i>Window frames: light and dark, active and inactive</i></td>
</tr> </tr>
<tr>
<td align="center"><img src="images/installer-welcome.png" width="300" alt="Installer" /></td>
<td align="center"><img src="images/installer-partitions.png" width="300" alt="Disk setup with Btrfs" /></td>
<td align="center"><img src="images/grub-snapshots.png" width="300" alt="Snapshots in the boot menu" /></td>
</tr>
<tr>
<td align="center"><i>Installer</i></td>
<td align="center"><i>Disk setup with Btrfs</i></td>
<td align="center"><i>Snapshots in the boot menu</i></td>
</tr>
<tr>
<td align="center"><img src="images/welcome.png" width="300" alt="Welcome window" /></td>
<td align="center"><img src="images/update.png" width="300" alt="System update" /></td>
<td align="center"><img src="images/server-installer.png" width="300" alt="Server installer" /></td>
</tr>
<tr>
<td align="center"><i>Welcome window</i></td>
<td align="center"><i>System update</i></td>
<td align="center"><i>Server installer</i></td>
</tr>
</table> </table>
### 📌 Using ### 📌 Using
@@ -56,6 +84,64 @@ Void Linux is fast, simple and independent: runit instead of systemd, and its ow
- Ukrainian interface (`uk_UA.UTF-8`) and the Kyiv time zone by default. Keyboard layouts are `us,ua`, switched with Alt+Shift. - Ukrainian interface (`uk_UA.UTF-8`) and the Kyiv time zone by default. Keyboard layouts are `us,ua`, switched with Alt+Shift.
- Includes NetworkManager, Bluetooth, PipeWire sound and Firefox with the Ukrainian language pack. Services run under runit: `dbus`, `elogind`, `polkitd`, `NetworkManager`, `bluetoothd`, `lightdm`. - Includes NetworkManager, Bluetooth, PipeWire sound and Firefox with the Ukrainian language pack. Services run under runit: `dbus`, `elogind`, `polkitd`, `NetworkManager`, `bluetoothd`, `lightdm`.
- Includes guest tools for QEMU/SPICE and VirtualBox. - Includes guest tools for QEMU/SPICE and VirtualBox.
- The *Install Das OS* icon on the desktop and in the menu starts the installer.
#### Graphical installer<a id='Installer' />
**_Calamares installs Das OS in a few steps: language and time zone, keyboard, disk, user._**
- The interface is in Ukrainian by default, with the Kyiv time zone and Das OS branding and slides.
- The disk can be erased automatically or partitioned by hand. Btrfs is the default, ext4 and XFS are also available. On UEFI machines it creates a 512 MiB EFI partition.
- It copies the clean live image (`/dev/mapper/live-base`), without the live user and its boot-time settings.
- After copying, it builds locales for the chosen language, keeps only the guest tools of the hypervisor it runs in (none on real hardware), sets up snapshots, builds the initramfs and installs GRUB.
- When it finishes, it removes itself and the packages that only it needed.
- Disk encryption is not offered yet.
#### Snapshots and rollback<a id='Snapshots' />
**_Btrfs snapshots keep the system recoverable after a bad update._**
- The installer creates the subvolumes `@` (root), `@home`, `@snapshots`, `@var_log` and `@var_cache_xbps`. Root snapshots don't include personal files, logs or the package cache.
- snapper makes a *Fresh install* snapshot right after installation and an hourly snapshot (cron). It keeps the last 5 hourly, 7 daily and 2 weekly ones.
- grub-btrfs adds every snapshot to the *Das OS snapshots* submenu in GRUB and updates it as snapshots appear.
- A snapshot is read-only. When it boots, the `das-snapshot-overlay` dracut module puts a writable overlay in RAM on top, so the full desktop works. Changes made there are lost at shutdown.
- `sudo snapper-rollback <number>` makes the snapshot the new root (the old one is kept as `@<date>`). The change takes effect after a reboot. It works both from the running system and from a booted snapshot.
- Members of `wheel` can list snapshots without sudo: `snapper list`.
#### System update<a id='Update' />
**_`das-update` installs updates between a pair of snapshots, so a bad update can be undone from the boot menu._**
- Before updating, it creates a snapper *pre* snapshot, then runs `xbps-install -Su` (updating xbps itself first when needed), then creates the paired *post* snapshot, also when the update failed.
- If the *pre* snapshot cannot be created, nothing is installed. Without Btrfs (ext4, XFS) it updates without a snapshot and says so.
- When it finishes, it shows the snapshot number and how to roll back to it, and asks for a reboot when the kernel, glibc, runit or other core packages were updated.
- The *System update* window lists the packages with the installed and new versions and shows the xbps output. The privileged part runs through pkexec (polkit action `ua.org.das.update`).
- In the background it checks for updates 2 minutes after login and then every 6 hours, without root. The tray icon and a notification appear only when updates are available. A click opens the window, and a right click opens a menu with *Check now*. The check is off in the live session.
- `das-update --cli` does the same in a terminal (servers, ssh). It asks before installing (`-y` skips the question) and runs itself again through sudo. Without a display it works in the terminal even without `--cli`.
- The command itself is the `das-update` package without GTK, the window and the tray icon are `/usr/libexec/das/das-update-gui` from `das-tools`.
#### Welcome window<a id='Welcome' />
**_`das-welcome` opens at the first login and gathers the first steps in one place._**
- In the live session it offers to install Das OS.
- The interface language can be chosen when the system has more than one locale built. It is stored in AccountsService and `~/.dmrc` and takes effect at the next login.
- If the home folders still have English names (`Documents`, `Downloads`…), it offers to rename them to the system language, together with `user-dirs.dirs` and the file manager bookmarks. Folders the user moved, and those whose new name is already taken, are left alone.
- It links to the system update, Xfce settings and help.
- It opens by itself only once. The *Show at login* check box keeps it opening at every login.
#### Server image<a id='Server' />
**_A console-only live ISO that installs a Das OS server with SSH, a firewall and Docker ready to use._**
- `dasos-server-<version>-x86_64.iso` boots on UEFI and BIOS with a login on the screen and on the serial console (`ttyS0`, 115200), so it also works through IPMI, VPS consoles and `qemu -nographic`. Log in as `live` with the password `live`, then run `sudo das-server-install`.
- The installer asks in Ukrainian or English: the disk, server name, administrator and password, an SSH key (and whether password logins stay allowed), time zone, console layout, language and network (dhcpcd or NetworkManager). `das-server-install --config FILE` installs without questions, see `/usr/share/examples/das-server-installer/install.conf`.
- It uses the same GPT and Btrfs layout as the desktop installer, so snapshots, the *Das OS snapshots* boot menu and `snapper-rollback` work the same way. GRUB is installed for UEFI (also at the fallback path) or BIOS, on the serial console too when the installer was started there.
- The administrator gets sudo through `wheel` and is in the `docker` and `socklog` groups. root has no password.
- The installed system runs `sshd`, `chronyd`, `nftables`, `fail2ban`, `docker`, `socklog` and the chosen network service.
- nftables lets in only SSH, ICMP and DHCPv6 (`/etc/nftables.conf`), more ports are opened with `dfw`. The rules live in their own table `inet das`, so reloading them keeps the Docker and fail2ban rules. Ports that containers publish pass through Docker's own rules.
- fail2ban bans addresses that keep failing SSH logins, reading the socklog log. sshd doesn't accept root password logins.
- Tools: Docker with Compose, lazydocker, lazygit, Midnight Commander, tmux, htop, Python 3, curl, `das-update --cli`, the [helpers](#Helpers) and xtools (`xi`, `xq`, `xrs`). There are no graphical libraries, and no firmware for NVIDIA graphics or sound cards (`/etc/xbps.d/das-server.conf` keeps updates from bringing it back).
#### Command-line helpers<a id='Helpers' />
**_Short commands for tasks that systemd distributions handle with systemctl, hostnamectl and the like, and that Void leaves to manual editing._**
- They answer in Ukrainian or English, run themselves again through sudo when they need root and have bash completion. In a terminal they ask for whatever is missing, and with all arguments given they work in scripts.
- `dsv` manages runit services: `list` (enabled or not, state, PID, uptime), `enable` and `disable` (at boot and right now), `start`, `stop`, `restart`, `status` and `logs [-f]` (the service's own svlogd log or its lines in socklog).
- `dsv new` creates a service for your own program: the command, user, working directory and environment variables go to `/etc/sv/NAME`, with a log in `/var/log/NAME`. It then starts the service and checks that it stays up. `dsv rm` deletes such a service and refuses to touch services from packages.
- `duser` manages accounts: `add` with the same groups as the desktop installer (`--admin` adds `wheel`, `docker` and `socklog`), a password and an SSH key; `passwd`, `lock` and `unlock` (a locked account can't log in with a key either), `key add|list|rm` for `authorized_keys`, `groups NAME +GROUP -GROUP` and `del`. It won't delete or lock out the current user, and it won't delete the only administrator.
- `dfw` opens ports in the server firewall: `dfw allow 80/tcp`, `dfw allow https 8000-8010/tcp`, `dfw deny 80/tcp`. The ports are kept in `/etc/nftables.d/dfw.nft` and checked with `nft -c` before loading, and only the `inet das` table is reloaded. `dfw status` also lists the ports Docker containers publish and the addresses fail2ban has banned. SSH always stays open.
- `dsys` replaces hostnamectl, timedatectl and localectl: `info` (time sync, language, console, addresses), `hostname`, `timezone` (part of the name will do: `dsys timezone Kyiv`), `locale` (builds the glibc locale if needed) and `keymap` (with a Cyrillic console font for Cyrillic layouts).
- They come in the `das-helpers` package and are on the desktop too. Both images also include `xtools-minimal` from Void for package work: `xi` installs, `xrs` searches, `xq` shows package details and `xcheckrestart` finds programs to restart after an update.
#### Das OS look<a id='Look' /> #### Das OS look<a id='Look' />
**_A modern minimal style with a blue and yellow eight-petal star from Ukrainian embroidery._** **_A modern minimal style with a blue and yellow eight-petal star from Ukrainian embroidery._**
@@ -70,30 +156,37 @@ Void Linux is fast, simple and independent: runit instead of systemd, and its ow
- One bottom panel holds the Whisker menu, window buttons, tray, keyboard layout, volume, notifications, clock and a show-desktop button. - One bottom panel holds the Whisker menu, window buttons, tray, keyboard layout, volume, notifications, clock and a show-desktop button.
- At the first login, `das-session-init` sets the wallpaper on every connected monitor, because Xfce stores wallpapers per monitor name. - At the first login, `das-session-init` sets the wallpaper on every connected monitor, because Xfce stores wallpapers per monitor name.
- LightDM uses the GTK greeter with the Das OS theme and wallpaper. - LightDM uses the GTK greeter with the Das OS theme and wallpaper.
- Home folders get names in the session language. xdg-user-dirs 0.20 looks for its translations only in the first `XDG_DATA_DIRS` entry that has a `locale` directory, the empty `/usr/local/share/locale`, so it created English folders. `/etc/X11/xinit/xinitrc.d/50-das-user-dirs` creates them before the Xfce session with a lookup path that finds the translations.
#### Own package repository<a id='Repository' /> #### Own package repository<a id='Repository' />
**_Every Das OS file is installed and updated by xbps._** **_Every Das OS file is installed and updated by xbps._**
- `srcpkgs/das-theme` contains the themes, xfwm4 frames, wallpapers and logo. - `srcpkgs/das-theme` contains the themes, xfwm4 frames, wallpapers and logo.
- `srcpkgs/das-settings` contains the Xfce, LightDM and session defaults. - `srcpkgs/das-settings` contains the Xfce, LightDM and session defaults.
- `srcpkgs/adw-gtk3` and `srcpkgs/font-jetbrains-mono` package upstream releases that Void does not have. - `srcpkgs/das-tools` contains `das-welcome` and the update window and tray icon (Go, GTK 3 through gotk3) with their menu and autostart entries. Its subpackage `das-update` is the update command and polkit action, without GTK.
- `srcpkgs/das-snapshots` contains the snapper configuration, `das-snapper-setup` and the dracut module for booting snapshots.
- `srcpkgs/das-installer` contains the Calamares settings and branding, `srcpkgs/das-server-installer` the console installer. The steps both share (`mount-source`, `postinstall`) are in `srcpkgs/das-install-common`. They exist only on the live ISOs.
- `srcpkgs/das-server` is the server metapackage with the nftables, fail2ban, sshd and Docker defaults. `srcpkgs/das-helpers` contains `dsv`, `duser`, `dfw` and `dsys`.
- `srcpkgs/calamares`, `srcpkgs/adw-gtk3` and `srcpkgs/font-jetbrains-mono` package upstream releases that Void does not have.
- `make packages` lays the templates over a shallow clone of void-packages and builds them with `xbps-src` into `out/hostdir/binpkgs`. The ISO build installs them from there. - `make packages` lays the templates over a shallow clone of void-packages and builds them with `xbps-src` into `out/hostdir/binpkgs`. The ISO build installs them from there.
#### Containerised build<a id='Build' /> #### Containerised build<a id='Build' />
**_The whole build runs in a Docker container, so only Docker and QEMU are needed on the host._** **_The whole build runs in a Docker container, so only Docker and QEMU are needed on the host._**
1. `make packages` builds the local xbps repository. 1. `make packages` builds the local xbps repository.
2. `make iso` builds the packages and then the ISO with `void-mklive`, using the profile in `live/` (package list, services, root file system overlay). 2. `make iso` builds the packages and then the ISO with `void-mklive`, using the profile in `live/` (package list, services, root file system overlay). `make iso-server` does the same for the server profile in `server/`.
3. `make qemu` boots the newest ISO with UEFI. `make qemu-bios` uses legacy BIOS, and `make qemu-disk` also attaches a 20 GB virtual disk. 3. `make qemu` boots the newest ISO with UEFI. `make qemu-bios` uses legacy BIOS, and `make qemu-disk` also attaches a 20 GB virtual disk to install on. `make qemu-server` boots the server ISO with the disk on a serial console in the terminal (Ctrl-A X quits).
### 🚀 Building the application ### 🚀 Building the application
Requirements: Linux with Docker (the user must be in the `docker` group), QEMU and OVMF for testing, and Go 1.22+ for `make test`. About 10 GB of free disk space. Requirements: Linux with Docker (the user must be in the `docker` group), QEMU and OVMF for testing, and Go 1.24+ with the GTK 3 development files for `make test`. About 10 GB of free disk space.
```shell ```shell
make iso # build packages and the live ISO (the first run downloads ~2 GB) make iso # build packages and the live ISO (the first run downloads ~2 GB)
make qemu # boot it in QEMU with UEFI make qemu # boot it in QEMU with UEFI
make iso-server # build packages and the server ISO
make qemu-server # boot it on a serial console with a disk to install on
make test # run Go tests make test # run Go tests
make clean # remove build output, keep the download cache make clean # remove build output, keep the download cache
``` ```
The ISO appears in `out/iso/dasos-<version>-x86_64.iso`. Write it to a USB stick with `dd` or Ventoy. The ISOs appear in `out/iso/dasos-<version>-x86_64.iso` and `out/iso/dasos-server-<version>-x86_64.iso`. Write them to a USB stick with `dd` or Ventoy.
**Note:** Das OS is an early version (0.1.0) and the live ISO cannot install itself yet. The Calamares installer, Btrfs snapshots with rollback, the Go utilities `das-welcome` and `das-update`, the developer profile and signed repository hosting are planned for upcoming stages. Das OS is based on Void Linux but is not affiliated with it. **Note:** Das OS is an early version (0.5.0). The developer profile and signed repository hosting are planned for upcoming stages. Until the repository is hosted, an installed system gets updates from Void only. Das OS is based on Void Linux but is not affiliated with it.
###### _Made by -=:dAs:=-_ ###### _Made by -=:dAs:=-_
+102 -9
View File
@@ -1,15 +1,21 @@
## Das OS ## Das OS
### _Легкий і дружній десктоп на базі Void Linux з Xfce та українським колоритом_ ### _Легкий і дружній десктоп на базі Void Linux з Xfce та українським колоритом, а також консольна серверна версія_
[![Void Linux](https://img.shields.io/badge/Void_Linux-x86__64_glibc-green)](https://voidlinux.org) [![Xfce](https://img.shields.io/badge/Xfce-4.20-orange)](https://xfce.org) [![runit](https://img.shields.io/badge/init-runit-blue)](https://docs.voidlinux.org/config/services/) [![Go](https://img.shields.io/badge/Go-1.22+-blue)](https://go.dev) [![Version](https://img.shields.io/badge/version-0.1.0-lightgrey)](#) [![Void Linux](https://img.shields.io/badge/Void_Linux-x86__64_glibc-green)](https://voidlinux.org) [![Xfce](https://img.shields.io/badge/Xfce-4.20-orange)](https://xfce.org) [![runit](https://img.shields.io/badge/init-runit-blue)](https://docs.voidlinux.org/config/services/) [![Go](https://img.shields.io/badge/Go-1.24+-blue)](https://go.dev) [![Docker](https://img.shields.io/badge/Docker-29-blue)](https://www.docker.com) [![Version](https://img.shields.io/badge/version-0.5.0-lightgrey)](#)
🇬🇧 [English version](README.md) 🇬🇧 [English version](README.md)
### 💡 Призначення ### 💡 Призначення
Void Linux швидкий, простий і незалежний: runit замість systemd і власний менеджер пакетів xbps. Проте з коробки це гола система, і щоб довести її до щоденного використання, треба кілька годин налаштовувати. Das OS збирає live-образ ISO, який одразу завантажується в налаштований десктоп Xfce. У ньому власне оформлення, український інтерфейс і розкладка та звичні програми. Усе, що додає Das OS, постачається власними xbps-пакетами, тож налаштування можна відтворити й оновлювати, як будь-який інший пакет. Void Linux швидкий, простий і незалежний: runit замість systemd і власний менеджер пакетів xbps. Проте з коробки це гола система, і щоб довести її до щоденного використання, треба кілька годин налаштовувати. Das OS збирає live-образ ISO, який одразу завантажується в налаштований десктоп Xfce і встановлюється графічним інсталятором. У ньому власне оформлення, український інтерфейс і розкладка, звичні програми та знімки Btrfs, які дозволяють скасувати невдале оновлення. Усе, що додає Das OS, постачається власними xbps-пакетами, тож налаштування можна відтворити й оновлювати, як будь-який інший пакет. Другий, консольний ISO встановлює сервер із налаштованими SSH, файрволом, fail2ban і Docker на тій самій розмітці Btrfs зі знімками.
### 📃 Можливості ### 📃 Можливості
- [Live ISO](#LiveISO), що завантажується з UEFI і BIOS одразу в робочий стіл - [Live ISO](#LiveISO), що завантажується з UEFI і BIOS одразу в робочий стіл
- [Графічний інсталятор](#Installer) Calamares, налаштований під Void і runit
- [Знімки й відкат](#Snapshots): щогодинні знімки Btrfs, які можна завантажити з меню GRUB
- [Оновлення системи](#Update) зі знімком перед ним: вікно, значок у треї та консольний режим
- [Вікно привітання](#Welcome) під час першого входу: мова, теки, оновлення та довідка
- [Серверний образ](#Server): консольний live ISO з текстовим інсталятором, SSH, файрволом, fail2ban і Docker
- [Консольні помічники](#Helpers) для щоденного адміністрування: служби (`dsv`), користувачі (`duser`), брандмауер (`dfw`) і системні налаштування (`dsys`)
- [Оформлення Das OS](#Look): світла й темна теми, згенеровані рамки вікон, шпалери та логотип - [Оформлення Das OS](#Look): світла й темна теми, згенеровані рамки вікон, шпалери та логотип
- [Налаштування за замовчуванням](#Defaults) для Xfce, які не перезаписують стандартні файли - [Налаштування за замовчуванням](#Defaults) для Xfce, які не перезаписують стандартні файли
- [Власний репозиторій пакетів](#Repository), який xbps-src збирає з шаблонів у дереві проєкту - [Власний репозиторій пакетів](#Repository), який xbps-src збирає з шаблонів у дереві проєкту
@@ -17,9 +23,11 @@ Void Linux швидкий, простий і незалежний: runit зам
### ⭐ Переваги ### ⭐ Переваги
- **Void усередині** – швидке завантаження з runit, менеджер пакетів xbps і свіжі програми без ручного налаштування. - **Void усередині** – швидке завантаження з runit, менеджер пакетів xbps і свіжі програми без ручного налаштування.
- **Безпечні оновлення** – `das-update` робить знімок перед кожним оновленням, систему можна повернути до будь-якого знімка, а особисті файли в `/home` відкат не зачіпає.
- **Легкість** – без systemd, а з Xfce, LightDM і PipeWire система добре працює навіть на старих ноутбуках. - **Легкість** – без systemd, а з Xfce, LightDM і PipeWire система добре працює навіть на старих ноутбуках.
- **Нічого прихованого** – усі зміни постачаються звичайними xbps-пакетами, і `xbps-query -f das-settings` показує, що саме змінено. - **Нічого прихованого** – усі зміни постачаються звичайними xbps-пакетами, і `xbps-query -f das-settings` показує, що саме змінено.
- **Контроль у користувача** – налаштування за замовчуванням лежать у `/etc/xdg/das`, тож власні налаштування користувача завжди мають пріоритет і не скидаються після оновлень. - **Контроль у користувача** – налаштування за замовчуванням лежать у `/etc/xdg/das`, тож власні налаштування користувача завжди мають пріоритет і не скидаються після оновлень.
- **Одна основа для десктопа й сервера** – обидва образи мають спільні пакети, розмітку диска, знімки й `das-update`, тож сервер оновлюється й відкочується так само, як ноутбук.
- **Збирається будь-де** – збирання відбувається в контейнері Void, тому хостом може бути Mint, Ubuntu чи Fedora. - **Збирається будь-де** – збирання відбувається в контейнері Void, тому хостом може бути Mint, Ubuntu чи Fedora.
### 📱 Скріншоти ### 📱 Скріншоти
@@ -44,6 +52,26 @@ Void Linux швидкий, простий і незалежний: runit зам
<td align="center"><i>Темні шпалери</i></td> <td align="center"><i>Темні шпалери</i></td>
<td align="center"><i>Рамки вікон: світлі й темні, активні й неактивні</i></td> <td align="center"><i>Рамки вікон: світлі й темні, активні й неактивні</i></td>
</tr> </tr>
<tr>
<td align="center"><img src="images/installer-welcome.png" width="300" alt="Інсталятор" /></td>
<td align="center"><img src="images/installer-partitions.png" width="300" alt="Розмітка диска з Btrfs" /></td>
<td align="center"><img src="images/grub-snapshots.png" width="300" alt="Знімки в меню завантаження" /></td>
</tr>
<tr>
<td align="center"><i>Інсталятор</i></td>
<td align="center"><i>Розмітка диска з Btrfs</i></td>
<td align="center"><i>Знімки в меню завантаження</i></td>
</tr>
<tr>
<td align="center"><img src="images/welcome.png" width="300" alt="Вікно привітання" /></td>
<td align="center"><img src="images/update.png" width="300" alt="Оновлення системи" /></td>
<td align="center"><img src="images/server-installer.png" width="300" alt="Серверний інсталятор" /></td>
</tr>
<tr>
<td align="center"><i>Вікно привітання</i></td>
<td align="center"><i>Оновлення системи</i></td>
<td align="center"><i>Серверний інсталятор</i></td>
</tr>
</table> </table>
### 📌 Використання ### 📌 Використання
@@ -56,6 +84,64 @@ Void Linux швидкий, простий і незалежний: runit зам
- Український інтерфейс (`uk_UA.UTF-8`) і часовий пояс Києва за замовчуванням. Розкладки `us,ua`, перемикання Alt+Shift. - Український інтерфейс (`uk_UA.UTF-8`) і часовий пояс Києва за замовчуванням. Розкладки `us,ua`, перемикання Alt+Shift.
- Є NetworkManager, Bluetooth, звук PipeWire і Firefox з українською мовою. Сервіси працюють під runit: `dbus`, `elogind`, `polkitd`, `NetworkManager`, `bluetoothd`, `lightdm`. - Є NetworkManager, Bluetooth, звук PipeWire і Firefox з українською мовою. Сервіси працюють під runit: `dbus`, `elogind`, `polkitd`, `NetworkManager`, `bluetoothd`, `lightdm`.
- Є гостьові інструменти для QEMU/SPICE та VirtualBox. - Є гостьові інструменти для QEMU/SPICE та VirtualBox.
- Значок *Встановити Das OS* на робочому столі та в меню запускає інсталятор.
#### Графічний інсталятор<a id='Installer' />
**_Calamares встановлює Das OS за кілька кроків: мова й часовий пояс, клавіатура, диск, користувач._**
- Інтерфейс за замовчуванням український, часовий пояс Києва, з брендингом і слайдами Das OS.
- Диск можна очистити автоматично або розмітити вручну. За замовчуванням Btrfs, також доступні ext4 і XFS. На комп'ютерах з UEFI створюється EFI-розділ на 512 МіБ.
- Копіюється чистий live-образ (`/dev/mapper/live-base`), без live-користувача і його налаштувань завантаження.
- Після копіювання інсталятор збирає локалі для вибраної мови, лишає гостьові інструменти лише того гіпервізора, у якому працює (на реальному залізі — жодних), налаштовує знімки, збирає initramfs і встановлює GRUB.
- Наприкінці інсталятор видаляє себе й пакети, потрібні лише йому.
- Шифрування диска поки не пропонується.
#### Знімки й відкат<a id='Snapshots' />
**_Знімки Btrfs дозволяють відновити систему після невдалого оновлення._**
- Інсталятор створює підтоми `@` (корінь), `@home`, `@snapshots`, `@var_log` і `@var_cache_xbps`. Знімки кореня не містять особистих файлів, журналів і кешу пакетів.
- snapper робить знімок *Fresh install* одразу після встановлення і щогодинні знімки (cron). Зберігаються останні 5 щогодинних, 7 щоденних і 2 щотижневих.
- grub-btrfs додає кожен знімок у підменю *Das OS snapshots* у GRUB і оновлює його, щойно з'являється новий.
- Знімок доступний лише для читання. Коли він завантажується, dracut-модуль `das-snapshot-overlay` накладає зверху записуваний шар у RAM, тож повноцінно працює весь робочий стіл. Зміни, зроблені в такій сесії, зникають після вимкнення.
- `sudo snapper-rollback <номер>` робить знімок новим коренем (старий зберігається як `@<дата>`). Зміна набуває чинності після перезавантаження. Команда працює і з робочої системи, і із завантаженого знімка.
- Учасники групи `wheel` бачать знімки без sudo: `snapper list`.
#### Оновлення системи<a id='Update' />
**_`das-update` встановлює оновлення між парою знімків, тож невдале оновлення можна скасувати з меню завантаження._**
- Перед оновленням створює знімок snapper *pre*, потім запускає `xbps-install -Su` (спершу оновлює сам xbps, якщо треба), а наприкінці створює парний знімок *post*, навіть якщо оновлення не вдалося.
- Якщо знімок *pre* не вдалося створити, нічого не встановлюється. Без Btrfs (ext4, XFS) оновлює без знімка й попереджає про це.
- Після завершення показує номер знімка й те, як до нього повернутися, і просить перезавантажитися, якщо оновилися ядро, glibc, runit чи інші ключові пакети.
- Вікно *Оновлення системи* показує список пакетів зі встановленою і новою версіями та вивід xbps. Частина, якій потрібні права root, запускається через pkexec (дія polkit `ua.org.das.update`).
- У фоні перевіряє оновлення через 2 хвилини після входу, а далі кожні 6 годин, без прав root. Значок у треї й сповіщення з'являються лише тоді, коли є оновлення. Клік відкриває вікно, правий клік — меню з пунктом *Перевірити зараз*. У live-сесії перевірка вимкнена.
- `das-update --cli` робить те саме в терміналі (сервери, ssh). Перед встановленням питає підтвердження (`-y` його пропускає) і сам перезапускається через sudo. Без графічного дисплея працює в терміналі й без `--cli`.
- Сама команда — пакет `das-update` без GTK, а вікно й значок у треї — `/usr/libexec/das/das-update-gui` з `das-tools`.
#### Вікно привітання<a id='Welcome' />
**_`das-welcome` відкривається під час першого входу і збирає перші кроки в одному місці._**
- У live-сесії пропонує встановити Das OS.
- Мову інтерфейсу можна вибрати, якщо в системі зібрано більше однієї локалі. Вона зберігається в AccountsService і `~/.dmrc` і діє з наступного входу.
- Якщо теки домашнього каталогу лишилися з англійськими назвами (`Documents`, `Downloads`…), пропонує перейменувати їх мовою системи разом із `user-dirs.dirs` і закладками файлового менеджера. Теки, які користувач переніс, і ті, чия нова назва вже зайнята, не чіпає.
- Містить посилання на оновлення системи, налаштування Xfce і довідку.
- Сам відкривається лише один раз. Прапорець *Показувати під час входу* залишає його на кожен вхід.
#### Серверний образ<a id='Server' />
**_Консольний live ISO, що встановлює сервер Das OS з готовими до роботи SSH, файрволом і Docker._**
- `dasos-server-<версія>-x86_64.iso` завантажується з UEFI і BIOS із входом і на екрані, і на послідовній консолі (`ttyS0`, 115200), тож працює й через IPMI, консолі VPS та `qemu -nographic`. Увійдіть як `live` з паролем `live` і запустіть `sudo das-server-install`.
- Інсталятор питає українською або англійською: диск, ім'я сервера, адміністратора й пароль, SSH-ключ (і чи лишати вхід за паролем), часовий пояс, розкладку консолі, мову та мережу (dhcpcd або NetworkManager). `das-server-install --config ФАЙЛ` встановлює без запитань, приклад — `/usr/share/examples/das-server-installer/install.conf`.
- Розмітка GPT і Btrfs та сама, що в десктопному інсталяторі, тож знімки, меню *Das OS snapshots* і `snapper-rollback` працюють так само. GRUB встановлюється для UEFI (також за запасним шляхом) або BIOS, а якщо інсталятор запущено з послідовної консолі, то й на неї.
- Адміністратор отримує sudo через `wheel` і входить у групи `docker` та `socklog`. У root немає пароля.
- Встановлена система запускає `sshd`, `chronyd`, `nftables`, `fail2ban`, `docker`, `socklog` і вибрану мережеву службу.
- nftables пропускає лише SSH, ICMP і DHCPv6 (`/etc/nftables.conf`), інші порти відкриває `dfw`. Правила лежать у власній таблиці `inet das`, тож їх перезавантаження не чіпає правил Docker і fail2ban. Порти, які публікують контейнери, проходять через власні правила Docker.
- fail2ban блокує адреси, з яких раз у раз не вдається увійти по SSH, читаючи журнал socklog. sshd не приймає вхід root за паролем.
- Інструменти: Docker з Compose, lazydocker, lazygit, Midnight Commander, tmux, htop, Python 3, curl, `das-update --cli`, [помічники](#Helpers) і xtools (`xi`, `xq`, `xrs`). Графічних бібліотек немає, як і прошивок для відеокарт NVIDIA та звукових карт (`/etc/xbps.d/das-server.conf` не дає оновленням повернути їх).
#### Консольні помічники<a id='Helpers' />
**_Короткі команди для задач, які в дистрибутивах із systemd розв'язують systemctl, hostnamectl тощо, а у Void доводиться робити вручну._**
- Вони відповідають українською або англійською, коли потрібні права root, самі перезапускаються через sudo, і мають автодоповнення bash. У терміналі вони запитують те, чого не вказано, а з усіма аргументами працюють у скриптах.
- `dsv` керує службами runit: `list` (чи ввімкнена, стан, PID, час роботи), `enable` і `disable` (під час завантаження й одразу), `start`, `stop`, `restart`, `status` і `logs [-f]` (власний журнал служби svlogd або її рядки в socklog).
- `dsv new` створює службу для власної програми: команда, користувач, робочий каталог і змінні середовища записуються в `/etc/sv/НАЗВА`, журнал ведеться в `/var/log/НАЗВА`. Потім служба запускається, і `dsv` перевіряє, що вона працює стабільно. `dsv rm` видаляє таку службу, а служби з пакетів не чіпає.
- `duser` керує обліковими записами: `add` з тими ж групами, що дає десктопний інсталятор (`--admin` додає `wheel`, `docker` і `socklog`), паролем і SSH-ключем; `passwd`, `lock` і `unlock` (заблокований запис не ввійде й за ключем), `key add|list|rm` для `authorized_keys`, `groups ІМ'Я +ГРУПА -ГРУПА` і `del`. Не дасть видалити чи заблокувати поточного користувача та видалити єдиного адміністратора.
- `dfw` відкриває порти в серверному брандмауері: `dfw allow 80/tcp`, `dfw allow https 8000-8010/tcp`, `dfw deny 80/tcp`. Порти зберігаються в `/etc/nftables.d/dfw.nft` і перевіряються через `nft -c` перед завантаженням, а перезавантажується лише таблиця `inet das`. `dfw status` показує ще й порти, опубліковані контейнерами Docker, та адреси, заблоковані fail2ban. SSH лишається відкритим завжди.
- `dsys` замінює hostnamectl, timedatectl і localectl: `info` (синхронізація часу, мова, консоль, адреси), `hostname`, `timezone` (досить частини назви: `dsys timezone Kyiv`), `locale` (збирає локаль glibc, якщо треба) і `keymap` (з кириличним шрифтом консолі для кириличних розкладок).
- Вони входять у пакет `das-helpers` і є й на десктопі. Обидва образи також містять `xtools-minimal` з Void для роботи з пакетами: `xi` встановлює, `xrs` шукає, `xq` показує відомості про пакет, а `xcheckrestart` знаходить програми, які треба перезапустити після оновлення.
#### Оформлення Das OS<a id='Look' /> #### Оформлення Das OS<a id='Look' />
**_Сучасний мінімалізм із синьо-жовтою восьмипелюстковою зіркою з української вишивки._** **_Сучасний мінімалізм із синьо-жовтою восьмипелюстковою зіркою з української вишивки._**
@@ -70,30 +156,37 @@ Void Linux швидкий, простий і незалежний: runit зам
- Одна нижня панель містить меню Whisker, кнопки вікон, трей, розкладку клавіатури, гучність, сповіщення, годинник і кнопку «показати стільницю». - Одна нижня панель містить меню Whisker, кнопки вікон, трей, розкладку клавіатури, гучність, сповіщення, годинник і кнопку «показати стільницю».
- При першому вході `das-session-init` встановлює шпалери на кожному підключеному моніторі, бо Xfce зберігає шпалери за назвою монітора. - При першому вході `das-session-init` встановлює шпалери на кожному підключеному моніторі, бо Xfce зберігає шпалери за назвою монітора.
- LightDM використовує GTK-вітальню з темою і шпалерами Das OS. - LightDM використовує GTK-вітальню з темою і шпалерами Das OS.
- Теки домашнього каталогу отримують назви мовою сеансу. xdg-user-dirs 0.20 шукає переклади лише в першій теці з `XDG_DATA_DIRS`, де є підкаталог `locale`, — порожній `/usr/local/share/locale`, тому створював теки англійською. `/etc/X11/xinit/xinitrc.d/50-das-user-dirs` створює їх ще до сеансу Xfce зі шляхом пошуку, де переклади знаходяться.
#### Власний репозиторій пакетів<a id='Repository' /> #### Власний репозиторій пакетів<a id='Repository' />
**_Кожен файл Das OS встановлює й оновлює xbps._** **_Кожен файл Das OS встановлює й оновлює xbps._**
- `srcpkgs/das-theme` містить теми, рамки xfwm4, шпалери й логотип. - `srcpkgs/das-theme` містить теми, рамки xfwm4, шпалери й логотип.
- `srcpkgs/das-settings` містить налаштування Xfce, LightDM і сесії. - `srcpkgs/das-settings` містить налаштування Xfce, LightDM і сесії.
- `srcpkgs/adw-gtk3` і `srcpkgs/font-jetbrains-mono` пакують релізи з GitHub, яких немає у Void. - `srcpkgs/das-tools` містить `das-welcome`, вікно оновлення і значок у треї (Go, GTK 3 через gotk3) з пунктами меню й автозапуском. Його підпакет `das-update` — сама команда оновлення й дія polkit, без GTK.
- `srcpkgs/das-snapshots` містить конфігурацію snapper, `das-snapper-setup` і dracut-модуль для завантаження знімків.
- `srcpkgs/das-installer` містить налаштування й брендинг Calamares, `srcpkgs/das-server-installer` — консольний інсталятор. Спільні для обох кроки (`mount-source`, `postinstall`) лежать у `srcpkgs/das-install-common`. Вони є лише на live-ISO.
- `srcpkgs/das-server` — серверний метапакет із налаштуваннями nftables, fail2ban, sshd і Docker. `srcpkgs/das-helpers` містить `dsv`, `duser`, `dfw` і `dsys`.
- `srcpkgs/calamares`, `srcpkgs/adw-gtk3` і `srcpkgs/font-jetbrains-mono` пакують релізи, яких немає у Void.
- `make packages` накладає шаблони на неглибокий клон void-packages і збирає їх через `xbps-src` в `out/hostdir/binpkgs`. Збирання ISO встановлює їх звідти. - `make packages` накладає шаблони на неглибокий клон void-packages і збирає їх через `xbps-src` в `out/hostdir/binpkgs`. Збирання ISO встановлює їх звідти.
#### Збирання в контейнері<a id='Build' /> #### Збирання в контейнері<a id='Build' />
**_Усе збирання відбувається в Docker-контейнері, тож на хості потрібні лише Docker і QEMU._** **_Усе збирання відбувається в Docker-контейнері, тож на хості потрібні лише Docker і QEMU._**
1. `make packages` збирає локальний xbps-репозиторій. 1. `make packages` збирає локальний xbps-репозиторій.
2. `make iso` збирає пакети, а потім ISO через `void-mklive` за профілем у `live/` (список пакетів, сервіси, оверлей кореневої файлової системи). 2. `make iso` збирає пакети, а потім ISO через `void-mklive` за профілем у `live/` (список пакетів, сервіси, оверлей кореневої файлової системи). `make iso-server` робить те саме для серверного профілю в `server/`.
3. `make qemu` завантажує найновіший ISO з UEFI. `make qemu-bios` використовує старий BIOS, а `make qemu-disk` ще й підключає віртуальний диск на 20 ГБ. 3. `make qemu` завантажує найновіший ISO з UEFI. `make qemu-bios` використовує старий BIOS, а `make qemu-disk` ще й підключає віртуальний диск на 20 ГБ для встановлення. `make qemu-server` завантажує серверний ISO з диском на послідовній консолі прямо в терміналі (вихід — Ctrl-A X).
### 🚀 Збирання застосунку ### 🚀 Збирання застосунку
Вимоги: Linux із Docker (користувач має бути в групі `docker`), QEMU та OVMF для тестування, Go 1.22+ для `make test`. Приблизно 10 ГБ вільного місця. Вимоги: Linux із Docker (користувач має бути в групі `docker`), QEMU та OVMF для тестування, Go 1.24+ і файли розробки GTK 3 для `make test`. Приблизно 10 ГБ вільного місця.
```shell ```shell
make iso # зібрати пакети й live-ISO (перший запуск завантажує ~2 ГБ) make iso # зібрати пакети й live-ISO (перший запуск завантажує ~2 ГБ)
make qemu # запустити його в QEMU з UEFI make qemu # запустити його в QEMU з UEFI
make iso-server # зібрати пакети й серверний ISO
make qemu-server # запустити його на послідовній консолі з диском для встановлення
make test # запустити Go-тести make test # запустити Go-тести
make clean # видалити результати збирання, зберегти кеш завантажень make clean # видалити результати збирання, зберегти кеш завантажень
``` ```
ISO з'являється в `out/iso/dasos-<версія>-x86_64.iso`. Записати його на флешку можна через `dd` або Ventoy. ISO з'являються в `out/iso/dasos-<версія>-x86_64.iso` і `out/iso/dasos-server-<версія>-x86_64.iso`. Записати їх на флешку можна через `dd` або Ventoy.
**Примітка:** Das OS — рання версія (0.1.0), і live-ISO поки не вміє встановлюватися на диск. Інсталятор Calamares, знімки Btrfs із відкатом, Go-утиліти `das-welcome` і `das-update`, профіль розробника та підписаний репозиторій на хостингу заплановані на наступні етапи. Das OS базується на Void Linux, але не пов'язана з ним офіційно. **Примітка:** Das OS — рання версія (0.5.0). Профіль розробника та підписаний репозиторій на хостингу заплановані на наступні етапи. Поки репозиторій не розміщено на хостингу, встановлена система отримує оновлення лише з Void. Das OS базується на Void Linux, але не пов'язана з ним офіційно.
###### _Made by -=:dAs:=-_ ###### _Made by -=:dAs:=-_
+86
View File
@@ -0,0 +1,86 @@
# Das OS — що зробити далі
Стан на 2026-10-20: версія 0.5.0, етапи 1–7 готові (live ISO, власний репозиторій, оформлення, інсталятор Calamares зі знімками Btrfs, Go-утиліти `das-update` і `das-welcome`, серверний образ з консольним інсталятором, менші ISO, консольні помічники). Далі: профіль розробника (етап 8).
## Етап 5. Go-утиліти (готово, лишилися перевірки)
- [x] `das-update`: вікно, значок у треї (лише коли є оновлення), консольний режим `--cli`, знімок snapper pre/post, встановлення через pkexec, підказка про відкат.
- [x] `das-welcome`: вікно першого входу (встановлення в live-сесії, мова, теки, оновлення, налаштування, довідка).
- [x] Англійські назви тек: xdg-user-dirs 0.20 шукає переклади лише в першій теці `XDG_DATA_DIRS` з підкаталогом `locale` (порожня `/usr/local/share/locale`). Виправлено в `das-settings` (`/etc/X11/xinit/xinitrc.d/50-das-user-dirs`), уже створені теки перейменовує `das-welcome`.
- [x] Тести `go test ./...` (`make test` і під час збирання пакета).
- [x] Перевірено у встановленій системі на Btrfs (QEMU, UEFI): оновлення з вікна й `--cli` створює пару знімків pre/post, вони з'являються в меню GRUB, завантаження в знімок і `snapper-rollback` повертають попередню версію пакета; вибір мови в `das-welcome` діє після перезавантаження.
- [x] Виправлено після перевірки: версія в Calamares береться з `VERSION`; значок у треї ховається, коли оновлення встановлено поза ним (стежить за базою пакетів xbps); інструменти VirtualBox прибираються з не-VirtualBox машин (у KVM було повідомлення при кожному вході); live-сесія не блокує екран під час встановлення.
- [x] Live ISO 0.4.0: екран не блокується під час встановлення.
- [ ] Вікно автентифікації polkit (`xfce-polkit`) англійською: перекласти або замінити агент.
- [ ] Повідомити авторів xdg-user-dirs про помилку з `XDG_DATA_DIRS`.
- [ ] Пункт «Профіль розробника» в `das-welcome` (разом з етапом 8).
## Етап 6. Серверний образ (готово)
Live ISO без графіки з консольним інсталятором: `dasos-server-<версія>-x86_64.iso`.
- [x] Профіль `server/` поруч із `live/`; `build-iso.sh [desktop|server]`, цілі `make iso-server` і `make qemu-server`. Kernel cmdline `console=tty0 console=ttyS0,115200`, вхід і на послідовній консолі.
- [x] Метапакет `das-server`: openssh, chrony, dhcpcd, NetworkManager, docker + docker-compose, nftables, fail2ban, socklog-void, python3, curl, lazydocker, lazygit, mc, tmux, htop, das-snapshots, das-update, das-helpers, xtools-minimal.
- [x] Налаштування: nftables «закрито все, крім SSH» (лише своя таблиця `inet das`, правила Docker і fail2ban не чіпає), jail sshd через nftables з розбором журналу socklog, sshd `PermitRootLogin prohibit-password`, журнали Docker з ротацією (`local`).
- [x] `dsv`: `list`, `status`, `enable`, `disable`, `start`, `stop`, `restart`, `logs [-f]`, автодоповнення bash; є й на десктопі.
- [x] `das-update` окремим підпакетом без GTK (вікно й трей — `/usr/libexec/das/das-update-gui` у `das-tools`).
- [x] `das-server-install`: TUI на `dialog` українською й англійською (диск, ім'я, користувач і пароль, SSH-ключ і заборона паролів, часовий пояс, розкладка, мова, dhcpcd/NetworkManager) і режим без запитань `--config`. GPT + Btrfs з тими ж підтомами, GRUB для UEFI (і запасний `BOOTX64.EFI`) та BIOS.
- [x] Спільний пакет `das-install-common` (`mount-source`, `postinstall`) для обох інсталяторів; `das-snapper-setup` вмикає `dbus`, без нього snapper не працює.
- [x] `scripts/run-qemu.sh --serial`.
- [x] Перевірено в QEMU: встановлення через TUI на UEFI (з SSH-ключем) і BIOS (з паролем, NetworkManager, `ua-utf`), вхід по SSH лише за ключем, `docker run hello-world`, опублікований порт nginx, fail2ban бачить невдалі входи, `das-update --cli` зі знімками pre/post, `dsv`.
- [x] `das-server-install --config` перевірено (0.5.0, UEFI).
- [x] Розмір ISO: див. нижче.
### Розмір ISO: 0.4.0 → 0.5.0
Десктоп 1,96 → 1,72 ГБ, сервер 1,57 → 1,14 ГБ.
- [x] `initrd` live-образу без драйверів `amdgpu`/`radeon`/`nouveau`/`i915`/`xe` і їхніх прошивок (`--omit-drivers` у виклику dracut у mklive): 245 → 89 МБ в обох ISO. Драйвери завантажуються з кореневої системи; десктоп у QEMU доходить до робочого столу.
- [x] Сервер без `linux-firmware-nvidia`, `sof-firmware`, `alsa-firmware`: `mklive -g` і `/etc/xbps.d/das-server.conf` (`ignorepkg`), тож оновлення їх не повертають. Службовий `mklive-ignore.conf` інсталятори видаляють.
- [x] squashfs з блоком 1 МБ і фільтром BCJ x86: десктоп −5 %; сервер разом із прошивками 1274 → 999 МБ.
- [ ] Перевірити на реальному залізі з AMD/Intel/NVIDIA, що графіка вмикається після `switch_root` (у QEMU — virtio-gpu).
- [ ] Переклади `/usr/share/locale` (265 МБ на десктопі, 98 МБ на сервері) лише для uk і en через `noextract` у xbps: ≈ −60…80 МБ на десктопі. Мінус: інші мови інтерфейсу доведеться доставляти перевстановленням пакетів.
- [ ] Десктоп: зайві варіанти Papirus (`ePapirus*`, `Papirus-Light`) з 294 МБ іконок; з'ясувати, хто тягне gtk4, flite, ghostscript.
- Не чіпати: Firefox (299 МБ), ядро й модулі (180 МБ), LLVM для Mesa (141 МБ), мережеві прошивки (442 МБ, Wi-Fi та мережеві карти).
## Етап 7. Консольні помічники (готово)
Пакет `das-helpers` (на десктопі й сервері), спільні функції в `/usr/lib/das/helpers.sh`, українська й англійська, автодоповнення bash, запитання в терміналі й аргументи для скриптів.
- [x] `dsv` переїхав у `das-helpers`; `dsv new` (команда, користувач, каталог, змінні, журнал svlogd, перевірка, що служба не падає) і `dsv rm` (лише не з пакетів).
- [x] `duser`: `list`, `add` (групи як у Calamares, `--admin`, ключ, пароль), `del`, `passwd`, `lock`/`unlock` (і вхід за ключем), `key add|list|rm`, `groups +/-`; захист від видалення чи блокування себе та єдиного адміністратора.
- [x] `dfw`: `status`, `allow`, `deny`, `reload`; набори `tcp_ports`/`udp_ports` у таблиці `inet das`, порти в `/etc/nftables.d/dfw.nft`, перевірка `nft -c`; показує порти Docker і заблоковані fail2ban адреси.
- [x] `dsys`: `info`, `hostname`, `timezone`, `locale`, `keymap`.
- [x] `xtools-minimal` (`xi`, `xq`, `xrs`, `xcheckrestart`) в обох образах.
- [x] Виправлено: коментар у `nftables.conf` радив `dsv restart nftables`, а `finish` служби Void робить `nft flush ruleset` і стирає правила fail2ban та Docker. Тепер `dfw reload`.
- [x] Перевірено на встановленому сервері: `dsv new` з Python-сервером на 8080, доступ з хоста після `dfw allow` і закриття після `dfw deny`; `duser add` з ключем, вхід, `lock` блокує й вхід за ключем; `dsys` (часовий пояс, ім'я, мова, розкладка); `dfw reload` поруч із контейнером nginx.
- [x] Повідомлення наприкінці `das-server-install` згадує `dsv`, `duser`, `dfw`, `dsys`.
## Етап 8. Профіль розробника
- [ ] Метапакет `das-dev`: go, git, docker, podman, vscode.
- [ ] Android Studio та JetBrains на вимогу через Flatpak.
- [ ] Вибір профілю в інсталяторі (модуль packagechooser) або в `das-welcome`.
## Етап 9. CI
- [ ] Збирання пакетів і обох ISO (десктоп і сервер) в контейнері Void (Gitea Actions на git.das.org.ua або GitHub Actions).
## Репозиторій пакетів
- [ ] Підпис через `xbps-rindex --sign`, ключ поза репозиторієм.
- [ ] Хостинг репозиторію (власний сервер або GitHub Releases/Pages).
- [ ] `/etc/xbps.d/das.conf` у встановленій системі, щоб пакети `das-*` оновлювалися. Зараз встановлена система бачить лише репозиторій Void.
## Інсталятор
- [ ] Перевірити встановлення десктопа на старий BIOS (MBR, `grub-install --target=i386-pc`); серверний інсталятор на BIOS перевірено.
- [ ] Перевірити на реальному залізі: видалення гостьових інструментів VM, Wi-Fi, NVIDIA/AMD.
- [ ] Шифрування диска (LUKS): `luksbootkeyfile`, crypttab, dracut, `GRUB_ENABLE_CRYPTODISK`.
- [ ] Перевірити ручну розмітку з Btrfs (чи створюються підтоми) і встановлення на ext4/XFS (знімки мають вимкнутися без помилок).
- [ ] Перевірити розкладку, відмінну від `us,ua` (автологін перевірено).
## Знімки й відкат
- [ ] Перевірити, що cron справді робить щогодинні знімки й очищує старі.
- [ ] Графічний відкат (у `das-update` або окремою утилітою) замість `sudo snapper-rollback N`.
- [ ] Прибирати старі корені `@<дата>`, які лишаються після відкату.
## Оформлення та дрібниці
- [ ] Прибрати значки томів live-носія («Том 6,6 ГБ») з робочого столу live-сесії.
- [ ] Тема GRUB у встановленій системі (зараз стандартний чорний екран).
- [ ] Перевірити меню Whisker вручну.
- [ ] Прискорити `make packages`: не перезбирати Calamares, якщо його версія не змінилася.
## Відкладено
- [ ] ARM-образ для Raspberry Pi.
- [ ] Вбудувати текстовий редактор MultiText.
- [ ] Перший стабільний реліз: злиття `dev` → `main`.
+1 -1
View File
@@ -1 +1 @@
0.1.0 0.5.0
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

+2
View File
@@ -0,0 +1,2 @@
Das OS \r (\l)
@@ -0,0 +1,8 @@
[Desktop Entry]
# Replaces the file from virtualbox-ose-guest: start the VirtualBox clients
# only inside VirtualBox, other hypervisors get a "kernel service is not
# running" notification at every login.
Type=Application
Name=VirtualBox User Session
Exec=sh -c "grep -qs VirtualBox /sys/class/dmi/id/product_name && exec /usr/bin/VBoxClient-all"
NoDisplay=true
+11 -1
View File
@@ -41,14 +41,19 @@ udisks2
xdg-user-dirs xdg-user-dirs
xdg-utils xdg-utils
# System tools for the live medium # Boot loader and disk tools, also kept by the installed system
btrfs-progs btrfs-progs
cryptsetup cryptsetup
dosfstools dosfstools
efibootmgr
gptfdisk gptfdisk
grub
grub-x86_64-efi
ntfs-3g ntfs-3g
os-prober
parted parted
sudo sudo
xfsprogs
# Virtual machine guests # Virtual machine guests
qemu-ga qemu-ga
@@ -56,5 +61,10 @@ spice-vdagent
virtualbox-ose-guest virtualbox-ose-guest
# Das OS (local repository) # Das OS (local repository)
das-installer
das-settings das-settings
das-snapshots
das-theme das-theme
das-tools
das-helpers
xtools-minimal
+74 -16
View File
@@ -1,11 +1,40 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Builds the Das OS live ISO with void-mklive from the profile in live/. # Builds a Das OS live ISO with void-mklive from a profile directory:
# build-iso.sh the desktop image from live/
# build-iso.sh server the console server image from server/
# Runs inside the builder container, see scripts/builder.sh. # Runs inside the builder container, see scripts/builder.sh.
set -euo pipefail set -euo pipefail
profile=${1:-desktop}
ignore=""
repo=/work/out/hostdir/binpkgs repo=/work/out/hostdir/binpkgs
version=$(< /work/VERSION) version=$(< /work/VERSION)
iso=/work/out/iso/dasos-${version}-x86_64.iso
case $profile in
desktop)
dir=/work/live
iso=/work/out/iso/dasos-${version}-x86_64.iso
title="Das OS"
locale=uk_UA.UTF-8
cmdline="live.user=live quiet loglevel=3"
;;
server)
dir=/work/server
iso=/work/out/iso/dasos-server-${version}-x86_64.iso
title="Das OS Server"
locale=en_US.UTF-8
# Both consoles: a screen and a serial line (IPMI, VPS consoles, QEMU -nographic).
# mklive starts a login on ttyS0 when it sees console=ttyS0.
cmdline="live.user=live loglevel=4 console=tty0 console=ttyS0,115200"
# das-server keeps these packages out of the installed system; the image
# is built before das-server is installed, so tell mklive as well
ignore=$(sed -n 's/^ignorepkg=//p' /work/srcpkgs/das-server/files/xbps-ignore.conf | xargs)
;;
*)
echo "build-iso: unknown profile '$profile' (desktop, server)" >&2
exit 1
;;
esac
if ! compgen -G "$repo/*-repodata" >/dev/null; then if ! compgen -G "$repo/*-repodata" >/dev/null; then
echo "build-iso: the local package repository is missing, run 'make packages' first" >&2 echo "build-iso: the local package repository is missing, run 'make packages' first" >&2
@@ -21,17 +50,25 @@ work=$(mktemp -d)
mkdir -p /work/out/iso mkdir -p /work/out/iso
trap 'rm -rf "$work"; chown -R "$HOST_UID:$HOST_GID" /work/out/iso' EXIT trap 'rm -rf "$work"; chown -R "$HOST_UID:$HOST_GID" /work/out/iso' EXIT
# Root file system overlay: static files from live/include plus generated links # Root file system overlay: static files from <profile>/include plus generated links
include=$work/include include=$work/include
cp -a /work/live/include "$include" mkdir -p "$include/etc"
[[ -d $dir/include ]] && cp -a "$dir/include/." "$include"
ln -sf /usr/share/zoneinfo/Europe/Kyiv "$include/etc/localtime" ln -sf /usr/share/zoneinfo/Europe/Kyiv "$include/etc/localtime"
# PipeWire as the sound server, wired up the same way as Void's own live images if [[ $profile == desktop ]]; then
mkdir -p "$include"/etc/xdg/autostart "$include"/etc/pipewire/pipewire.conf.d "$include"/etc/alsa/conf.d # PipeWire as the sound server, wired up the same way as Void's own live images
ln -sf /usr/share/applications/pipewire.desktop "$include"/etc/xdg/autostart/ mkdir -p "$include"/etc/xdg/autostart "$include"/etc/pipewire/pipewire.conf.d "$include"/etc/alsa/conf.d
ln -sf /usr/share/examples/wireplumber/10-wireplumber.conf "$include"/etc/pipewire/pipewire.conf.d/ ln -sf /usr/share/applications/pipewire.desktop "$include"/etc/xdg/autostart/
ln -sf /usr/share/examples/pipewire/20-pipewire-pulse.conf "$include"/etc/pipewire/pipewire.conf.d/ ln -sf /usr/share/examples/wireplumber/10-wireplumber.conf "$include"/etc/pipewire/pipewire.conf.d/
ln -sf /usr/share/alsa/alsa.conf.d/50-pipewire.conf "$include"/etc/alsa/conf.d/ ln -sf /usr/share/examples/pipewire/20-pipewire-pulse.conf "$include"/etc/pipewire/pipewire.conf.d/
ln -sf /usr/share/alsa/alsa.conf.d/99-pipewire-default.conf "$include"/etc/alsa/conf.d/ ln -sf /usr/share/alsa/alsa.conf.d/50-pipewire.conf "$include"/etc/alsa/conf.d/
ln -sf /usr/share/alsa/alsa.conf.d/99-pipewire-default.conf "$include"/etc/alsa/conf.d/
# The installer shows the Das OS version, which lives in VERSION, not in the package
branding=usr/share/calamares/branding/dasos
mkdir -p "$include/$branding"
sed -e "s/@VERSION@/$version/g" -e "s/@SHORT_VERSION@/${version%.*}/g" \
"/work/srcpkgs/das-installer/files/calamares/branding/dasos/branding.desc" >"$include/$branding/branding.desc"
fi
# mklive's dracut module creates the live user, hostname and passwords at boot. # mklive's dracut module creates the live user, hostname and passwords at boot.
# Rebrand its defaults in this container's copy (the image itself stays untouched). # Rebrand its defaults in this container's copy (the image itself stays untouched).
@@ -43,6 +80,26 @@ for pattern in 'echo void-live >' 'voidlinux'; do
fi fi
done done
sed -i -e 's/echo void-live >/echo dasos >/' -e 's/voidlinux/live/g' "$adduser" sed -i -e 's/echo void-live >/echo dasos >/' -e 's/voidlinux/live/g' "$adduser"
# mklive copies its /etc/issue (Void live logins) into the image, and the
# installers copy the image to disk, so use a Das OS one (the server's tells
# how to log in to the live system; its installer writes a neutral one).
cp "$dir/branding/issue" /opt/void-mklive/data/issue
# Size, also patched into this container's mklive copy:
# - the live initrd only has to find and mount the image, the GPU drivers and
# their firmware (~150 MB) load from the root file system after switch_root;
# - squashfs with 1 MB blocks and the x86 BCJ filter packs binaries tighter.
mklive=/opt/void-mklive/mklive.sh
for pattern in '--add-drivers "ahci"' '-comp "${SQUASHFS_COMPRESSION}"'; do
if ! grep -qF -- "$pattern" "$mklive"; then
echo "build-iso: '$pattern' not found in $mklive, mklive changed; update this script" >&2
exit 1
fi
done
sed -i \
-e 's/--add-drivers "ahci"/& --omit-drivers "amdgpu radeon nouveau i915 xe"/' \
-e 's/-comp "${SQUASHFS_COMPRESSION}"/& -b 1M -Xbcj x86/' \
"$mklive"
rsvg-convert /work/live/branding/splash.svg --output "$work/splash.png" rsvg-convert /work/live/branding/splash.svg --output "$work/splash.png"
rm -f "$iso" rm -f "$iso"
@@ -52,11 +109,12 @@ SPLASH_IMAGE=$work/splash.png ./mklive.sh \
-a x86_64 \ -a x86_64 \
-r "$repo" \ -r "$repo" \
-c /var/cache/xbps \ -c /var/cache/xbps \
-p "$(read_list /work/live/packages.x86_64)" \ -p "$(read_list "$dir/packages.x86_64")" \
-S "$(read_list /work/live/services)" \ -S "$(read_list "$dir/services")" \
${ignore:+-g "$ignore"} \
-I "$include" \ -I "$include" \
-l uk_UA.UTF-8 \ -l "$locale" \
-k us \ -k us \
-T "Das OS" \ -T "$title" \
-C "live.user=live quiet loglevel=3" \ -C "$cmdline" \
-o "$iso" -o "$iso"
+8
View File
@@ -41,6 +41,11 @@ for dir in /work/srcpkgs/*/; do
echo "warning: $name is now in void-packages, consider dropping our template" >&2 echo "warning: $name is now in void-packages, consider dropping our template" >&2
fi fi
rm -rf "$vp/srcpkgs/$name" rm -rf "$vp/srcpkgs/$name"
# A subpackage is a link to its source package's directory and is built with it
if [[ -L ${dir%/} ]]; then
as_builder ln -s "$(readlink "${dir%/}")" "$vp/srcpkgs/$name"
continue
fi
as_builder cp -rL "$dir" "$vp/srcpkgs/$name" as_builder cp -rL "$dir" "$vp/srcpkgs/$name"
names+=("$name") names+=("$name")
done done
@@ -50,6 +55,9 @@ cd "$vp"
if [[ ! -f masterdir-x86_64/.xbps_chroot_init ]]; then if [[ ! -f masterdir-x86_64/.xbps_chroot_init ]]; then
as_builder ./xbps-src -H "$hostdir" binary-bootstrap as_builder ./xbps-src -H "$hostdir" binary-bootstrap
fi fi
# A failed build leaves its destdir behind, and the next build of that package
# (also as a dependency of another one) would package the stale files
rm -rf masterdir-x86_64/destdir/*
for name in "${names[@]}"; do for name in "${names[@]}"; do
echo "==> Building $name" echo "==> Building $name"
as_builder ./xbps-src -H "$hostdir" -f pkg "$name" as_builder ./xbps-src -H "$hostdir" -f pkg "$name"
+17 -8
View File
@@ -1,16 +1,18 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Boots a Das OS ISO in QEMU (KVM when available). # Boots a Das OS ISO in QEMU (KVM when available).
# #
# Usage: scripts/run-qemu.sh [--bios] [--disk] [path/to/image.iso] # Usage: scripts/run-qemu.sh [--bios] [--disk] [--serial] [path/to/image.iso]
# --bios boot with legacy BIOS instead of UEFI # --bios boot with legacy BIOS instead of UEFI
# --disk attach out/qemu/disk.qcow2 (20 GB, created on first use) to test installing # --disk attach out/qemu/disk.qcow2 (20 GB, created on first use) to test installing
# Without a path the newest ISO in out/iso is used. # --serial no window: the serial console in this terminal (server ISO), Ctrl-A X quits
# Without a path the newest desktop ISO in out/iso is used (server ISO with --serial).
set -euo pipefail set -euo pipefail
root=$(cd "$(dirname "$0")/.." && pwd) root=$(cd "$(dirname "$0")/.." && pwd)
state=$root/out/qemu state=$root/out/qemu
firmware=uefi firmware=uefi
disk=false disk=false
serial=false
iso="" iso=""
die() { die() {
@@ -22,8 +24,9 @@ for arg in "$@"; do
case $arg in case $arg in
--bios) firmware=bios ;; --bios) firmware=bios ;;
--disk) disk=true ;; --disk) disk=true ;;
--serial) serial=true ;;
-h | --help) -h | --help)
sed -n '2,8s/^# \{0,1\}//p' "$0" sed -n '2,9s/^# \{0,1\}//p' "$0"
exit 0 exit 0
;; ;;
-*) die "unknown option: $arg" ;; -*) die "unknown option: $arg" ;;
@@ -32,17 +35,23 @@ for arg in "$@"; do
done done
if [[ -z $iso ]]; then if [[ -z $iso ]]; then
iso=$(ls -t "$root"/out/iso/*.iso 2>/dev/null | head -n 1) || true pattern="dasos-[0-9]*.iso"
[[ $serial == true ]] && pattern="dasos-server-*.iso"
iso=$(ls -t "$root"/out/iso/$pattern 2>/dev/null | head -n 1) || true
fi fi
[[ -n $iso && -f $iso ]] || die "no ISO found, run 'make iso' first" [[ -n $iso && -f $iso ]] || die "no ISO found, run 'make iso' first"
args=( args=(
-machine q35 -smp 4 -m 4096 -machine q35 -smp 4 -m 4096
-device virtio-vga -display gtk,zoom-to-fit=on
-device virtio-net-pci,netdev=net0 -netdev user,id=net0 -device virtio-net-pci,netdev=net0 -netdev user,id=net0
-device qemu-xhci -device usb-tablet
-cdrom "$iso" -boot order=d -cdrom "$iso" -boot order=d
) )
if [[ $serial == true ]]; then
# The guest's ttyS0 and the QEMU monitor share this terminal
args+=(-nographic -serial mon:stdio)
else
args+=(-device virtio-vga -display gtk,zoom-to-fit=on -device qemu-xhci -device usb-tablet)
fi
if [[ -w /dev/kvm ]]; then if [[ -w /dev/kvm ]]; then
args+=(-accel kvm -cpu host) args+=(-accel kvm -cpu host)
+4
View File
@@ -0,0 +1,4 @@
Das OS Server \r (\l)
Log in as "live" with the password "live", then run: sudo das-server-install
+20
View File
@@ -0,0 +1,20 @@
# Packages added to the Void base-system in the server live image.
# Das OS Server: SSH, firewall, Docker, tools (see srcpkgs/das-server)
das-server
# Installer and the disk tools it uses, also kept by the installed system
das-server-installer
btrfs-progs
cryptsetup
dosfstools
efibootmgr
gptfdisk
grub
grub-x86_64-efi
parted
sudo
xfsprogs
# Virtual machine guests
qemu-ga
+3
View File
@@ -0,0 +1,3 @@
# runit services enabled in the live image (/etc/sv/<name>). The installer
# enables the server's own services on the installed system.
dhcpcd
+22
View File
@@ -0,0 +1,22 @@
# Template file for 'calamares'
pkgname=calamares
version=3.4.3
revision=2
build_style=cmake
configure_args="-DWITH_QT6=ON -DWITH_QML=OFF -DWITH_PYTHON=ON -DWITH_PYBIND11=ON
-DINSTALL_CONFIG=OFF -DBUILD_TESTING=OFF -DBUILD_SCHEMA_TESTING=OFF
-DBUILD_CRASH_REPORTING=OFF -DCMAKE_INSTALL_LIBEXECDIR=libexec
-DUSE_services=none
-DSKIP_MODULES=dummycpp;dummyprocess;dummypython;initcpio;initcpiocfg;mkinitfs;openrcdmcryptcfg;plasmalnf;plymouthcfg;tracking;zfs;zfshostid;interactiveterminal"
hostmakedepends="extra-cmake-modules gettext pkg-config python3 qt6-base qt6-tools"
makedepends="icu-devel kf6-kcoreaddons-devel kpmcore-devel libparted-devel
libpwquality-devel libxcrypt-devel polkit-qt6-devel python3-devel qt6-base-devel
qt6-svg-devel qt6-tools-devel yaml-cpp-devel"
depends="ckbcomp kpmcore polkit python3 qt6-plugin-tls-openssl qt6-svg rsync
squashfs-tools xkeyboard-config"
short_desc="Distribution-independent installer framework"
maintainer="Das <anry.das@gmail.com>"
license="GPL-3.0-or-later"
homepage="https://calamares.codeberg.page"
distfiles="https://codeberg.org/Calamares/calamares/releases/download/v${version}/calamares-${version}.tar.gz"
checksum=144cbbf6bdcebfb21685950db4f0777218519df095f62f4aa392f28348110d18
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Das
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+240
View File
@@ -0,0 +1,240 @@
#!/bin/bash
# dfw - open and close ports in the Das OS server firewall (nftables).
#
# /etc/nftables.conf defines the "das" table with the sets tcp_ports and
# udp_ports; dfw keeps their elements in /etc/nftables.d/dfw.nft, which that
# file includes, and reloads only the "das" table. SSH is always open.
set -euo pipefail
# shellcheck source=helpers.sh
. "${DAS_HELPERS:-/usr/lib/das/helpers.sh}"
conf=/etc/nftables.conf
ports_file=/etc/nftables.d/dfw.nft
usage() {
if [[ -n $uk ]]; then
cat <<'EOF'
Використання: dfw КОМАНДА [ПОРТ...]
status стан брандмауера й відкриті порти (типова команда)
allow ПОРТ... відкрити порти для вхідних з'єднань
deny ПОРТ... закрити порти, відкриті через allow
reload перечитати /etc/nftables.conf після редагування
ПОРТ: 80, 80/tcp, 53/udp, 8000-8010/tcp або назва служби (http, https).
Без /udp мається на увазі tcp. SSH відкритий завжди.
Порти, які публікують контейнери Docker (-p 8080:80), відкриває сам Docker,
dfw на них не впливає.
EOF
else
cat <<'EOF'
Usage: dfw COMMAND [PORT...]
status firewall state and open ports (the default)
allow PORT... open ports for incoming connections
deny PORT... close ports opened with allow
reload load /etc/nftables.conf again after editing it
PORT: 80, 80/tcp, 53/udp, 8000-8010/tcp or a service name (http, https).
Without /udp it means tcp. SSH is always open.
Ports that Docker containers publish (-p 8080:80) are opened by Docker
itself, dfw does not affect them.
EOF
fi
}
check_config() {
grep -qs 'set tcp_ports' "$conf" ||
die "$conf не з Das OS Server (немає набору tcp_ports); dfw працює лише з ним" \
"$conf is not the Das OS Server one (no tcp_ports set); dfw only works with that"
}
# Opened ports of one protocol from the ports file, one per line
saved_ports() {
[[ -r $ports_file ]] || return 0
sed -n "s/^add element inet das $1_ports { \(.*\) }$/\1/p" "$ports_file" | tr -d ' ' | tr ',' '\n'
}
# parse_port SPEC: prints "PORT PROTO" with PORT a number or a FIRST-LAST range
parse_port() {
local spec=$1 port proto=tcp first last
if [[ $spec == */* ]]; then
port=${spec%/*}
proto=${spec##*/}
else
port=$spec
fi
[[ $proto == tcp || $proto == udp ]] || die "'$spec': протокол tcp або udp" "'$spec': the protocol is tcp or udp"
if [[ $port =~ ^[a-z][a-z0-9-]*$ ]]; then
# A service name from /etc/services
first=$(getent services "$port/$proto" | awk '{ print $2 }')
[[ -n $first ]] || die "'$spec': невідома служба $port/$proto" "'$spec': unknown service $port/$proto"
port=${first%/*}
fi
if [[ $port =~ ^([0-9]+)-([0-9]+)$ ]]; then
first=${BASH_REMATCH[1]}
last=${BASH_REMATCH[2]}
elif [[ $port =~ ^[0-9]+$ ]]; then
first=$port
last=$port
else
die "'$spec': порт — число, діапазон 8000-8010 або назва служби" \
"'$spec': a port is a number, a range like 8000-8010 or a service name"
fi
((first >= 1 && last <= 65535 && first <= last)) ||
die "'$spec': порти від 1 до 65535" "'$spec': ports are 1 to 65535"
if ((first == last)); then
echo "$first $proto"
else
echo "$first-$last $proto"
fi
}
firewall_on() {
[[ -e /etc/runit/runsvdir/default/nftables ]]
}
# save TCP_PORTS UDP_PORTS (newline-separated): writes the ports file, checks
# the whole configuration and loads it, restoring the old file on an error
save() {
local tcp udp backup=""
# sort -n -u would take 80 and 80-90 for duplicates
tcp=$(sort -u <<<"$1" | sort -n | grep . | paste -sd, - | sed 's/,/, /g' || true)
udp=$(sort -u <<<"$2" | sort -n | grep . | paste -sd, - | sed 's/,/, /g' || true)
mkdir -p "${ports_file%/*}"
if [[ -e $ports_file ]]; then
backup=$(mktemp)
cp "$ports_file" "$backup"
fi
{
echo "# Ports opened with dfw; change them with dfw allow/deny"
[[ -z $tcp ]] || echo "add element inet das tcp_ports { $tcp }"
[[ -z $udp ]] || echo "add element inet das udp_ports { $udp }"
} >"$ports_file"
if ! nft -c -f "$conf"; then
if [[ -n $backup ]]; then mv "$backup" "$ports_file"; else rm -f "$ports_file"; fi
die "nftables не прийняв правила, нічого не змінено" "nftables rejected the rules, nothing changed"
fi
rm -f "$backup"
if firewall_on; then
nft -f "$conf"
else
warn "брандмауер вимкнений, зміни збережено; увімкнути: dsv enable nftables" \
"the firewall is off, changes are saved; to turn it on: dsv enable nftables"
fi
}
cmd_allow() {
local spec port proto tcp udp
tcp=$(saved_ports tcp)
udp=$(saved_ports udp)
for spec in "$@"; do
read -r port proto < <(parse_port "$spec")
if [[ $port == 22 && $proto == tcp ]]; then
say "22/tcp (SSH) відкритий завжди" "22/tcp (SSH) is always open"
continue
fi
if grep -qx "$port" <<<"$(if [[ $proto == tcp ]]; then echo "$tcp"; else echo "$udp"; fi)"; then
say "$port/$proto: уже відкритий" "$port/$proto: already open"
continue
fi
if [[ $proto == tcp ]]; then tcp+=$'\n'$port; else udp+=$'\n'$port; fi
say "$port/$proto: відкрито" "$port/$proto: open"
done
save "$tcp" "$udp"
}
cmd_deny() {
local spec port proto tcp udp list
tcp=$(saved_ports tcp)
udp=$(saved_ports udp)
for spec in "$@"; do
read -r port proto < <(parse_port "$spec")
if [[ $port == 22 && $proto == tcp ]]; then
die "SSH не закривається через dfw, щоб не втратити доступ до сервера; правило — у $conf" \
"SSH is not closed with dfw, so as not to lose access to the server; the rule is in $conf"
fi
if [[ $proto == tcp ]]; then list=$tcp; else list=$udp; fi
if ! grep -qx "$port" <<<"$list"; then
say "$port/$proto: не був відкритий через dfw" "$port/$proto: was not opened with dfw"
continue
fi
list=$(grep -vx "$port" <<<"$list" || true)
if [[ $proto == tcp ]]; then tcp=$list; else udp=$list; fi
say "$port/$proto: закрито" "$port/$proto: closed"
done
save "$tcp" "$udp"
}
cmd_status() {
local p state banned ports
if firewall_on && nft list table inet das >/dev/null 2>&1; then
state=$(say "увімкнений" "on")
else
state=$(say "ВИМКНЕНИЙ (dsv enable nftables)" "OFF (dsv enable nftables)")
fi
say "Брандмауер: $state" "Firewall: $state"
say "Вхідні з'єднання закриті, крім:" "Incoming connections are closed except:"
echo " 22/tcp (ssh)"
for p in tcp udp; do
while read -r ports; do
[[ -n $ports ]] && echo " $ports/$p"
done < <(saved_ports "$p")
done
# Docker opens its published ports itself
if command -v docker >/dev/null && ports=$(docker ps --format '{{.Names}}: {{.Ports}}' 2>/dev/null) &&
grep -q -- '->' <<<"$ports"; then
say "Опубліковані контейнерами Docker (відкриті незалежно від dfw):" \
"Published by Docker containers (open regardless of dfw):"
grep -- '->' <<<"$ports" | sed 's/^/ /'
fi
if banned=$(fail2ban-client status sshd 2>/dev/null | sed -n 's/.*Currently banned:[[:space:]]*//p') &&
[[ -n $banned && $banned != 0 ]]; then
say "fail2ban: заблоковано адрес за невдалі входи SSH: $banned" \
"fail2ban: addresses banned for failed SSH logins: $banned"
fi
}
cmd=${1:-status}
[[ $# -gt 0 ]] && shift
case $cmd in
status | list | ls)
need_root status
check_config
cmd_status
;;
allow | open)
[[ $# -gt 0 ]] || die "вкажіть порт: dfw allow 80/tcp" "give a port: dfw allow 80/tcp"
need_root allow "$@"
check_config
cmd_allow "$@"
;;
deny | close)
[[ $# -gt 0 ]] || die "вкажіть порт: dfw deny 80/tcp" "give a port: dfw deny 80/tcp"
need_root deny "$@"
check_config
cmd_deny "$@"
;;
reload)
need_root reload
nft -c -f "$conf"
if firewall_on; then
nft -f "$conf"
say "правила завантажено" "rules loaded"
else
say "правила правильні, але брандмауер вимкнений: dsv enable nftables" \
"the rules are valid, but the firewall is off: dsv enable nftables"
fi
;;
-h | --help | help) usage ;;
*)
usage >&2
exit 1
;;
esac
@@ -0,0 +1,22 @@
# bash completion for dfw
# shellcheck shell=bash
_dfw() {
local cur=${COMP_WORDS[COMP_CWORD]}
if ((COMP_CWORD == 1)); then
mapfile -t COMPREPLY < <(compgen -W "status allow deny reload help" -- "$cur")
return
fi
case ${COMP_WORDS[1]} in
deny)
# The ports opened with dfw
local ports
ports=$(sed -n 's/^add element inet das \(tcp\|udp\)_ports { \(.*\) }$/\2 \1/p' /etc/nftables.d/dfw.nft 2>/dev/null |
awk '{ for (i = 1; i < NF; i++) { p = $i; sub(/,$/, "", p); print p "/" $NF } }')
mapfile -t COMPREPLY < <(compgen -W "$ports" -- "$cur")
;;
allow)
mapfile -t COMPREPLY < <(compgen -W "http https 80/tcp 443/tcp 53/udp" -- "$cur")
;;
esac
}
complete -F _dfw dfw
+429
View File
@@ -0,0 +1,429 @@
#!/bin/bash
# dsv - manage runit services on Das OS, the way systemctl does elsewhere.
#
# A service is a directory /etc/sv/<name>; it is enabled (started at boot) by
# linking it into the default runlevel, which runsvdir watches through
# /var/service. Logs come from the service's own svlogd directory or, for
# services that log to syslog, from socklog.
set -euo pipefail
# shellcheck source=helpers.sh
. "${DAS_HELPERS:-/usr/lib/das/helpers.sh}"
sv_dir=/etc/sv
runlevel=/etc/runit/runsvdir/default
# Outside a running system (a chroot, an image) /var/service may not exist
service_dir=/var/service
[[ -d $service_dir ]] || service_dir=$runlevel
socklog=/var/log/socklog
usage() {
if [[ -n $uk ]]; then
cat <<'EOF'
Використання: dsv КОМАНДА [СЛУЖБА...]
list усі служби: чи ввімкнена, стан, PID, час роботи
status СЛУЖБА... стан служби (sv status)
enable СЛУЖБА... запускати під час завантаження й запустити зараз
disable СЛУЖБА... не запускати під час завантаження й зупинити
start|stop|restart СЛУЖБА...
запустити, зупинити, перезапустити
logs СЛУЖБА [-f] журнал служби (-f: стежити за новими рядками)
new [СЛУЖБА] [ПАРАМЕТРИ]
створити службу для своєї програми; чого не
вказано, те запитає:
--cmd КОМАНДА що запускати (програма не має йти у фон)
--user КОРИСТУВАЧ від чийого імені
--dir КАТАЛОГ робочий каталог
--env ІМ'Я=ЗНАЧЕННЯ змінна середовища, можна кілька разів
--no-log без власного журналу в /var/log/СЛУЖБА
--enable одразу ввімкнути й запустити
rm СЛУЖБА вимкнути й видалити службу, створену через new
Служби — каталоги в /etc/sv. Без команди виконується list.
EOF
else
cat <<'EOF'
Usage: dsv COMMAND [SERVICE...]
list all services: enabled or not, state, PID, uptime
status SERVICE... service state (sv status)
enable SERVICE... start at boot and start now
disable SERVICE... do not start at boot and stop now
start|stop|restart SERVICE...
start, stop, restart
logs SERVICE [-f] the service log (-f: follow new lines)
new [SERVICE] [OPTIONS] create a service for your own program; asks for
what is not given:
--cmd COMMAND what to run (the program must stay in the foreground)
--user USER who runs it
--dir DIR working directory
--env NAME=VALUE an environment variable, may be repeated
--no-log no log of its own in /var/log/SERVICE
--enable enable and start it right away
rm SERVICE disable and delete a service made with new
Services are the directories in /etc/sv. Without a command, runs list.
EOF
fi
}
check_service() {
[[ -d $sv_dir/$1 ]] || die "немає служби '$1' (див. dsv list)" "no service '$1' (see dsv list)"
}
is_enabled() {
[[ -e $runlevel/$1 || -L $runlevel/$1 ]]
}
# Waits up to 10 s for runsvdir to pick up a new link (it rescans every 5 s)
wait_supervised() {
local i
for ((i = 0; i < 20; i++)); do
[[ -e $service_dir/$1/supervise/ok ]] && return 0
sleep 0.5
done
return 1
}
# row NAME ENABLED STATE PID UPTIME: one line of the list
row() {
pad "$1" 24
pad "$2" 9
pad "$3" 6
pad "$4" 8
printf '%s\n' "$5"
}
cmd_list() {
local dir name enabled state pid time line
row "$(say СЛУЖБА SERVICE)" "$(say УВІМКН. ENABLED)" "$(say СТАН STATE)" PID "$(say ПРАЦЮЄ UPTIME)"
for dir in "$sv_dir"/*/; do
name=$(basename "$dir")
enabled=$(say ні no)
state=-
pid=-
time=-
if is_enabled "$name"; then
enabled=$(say так yes)
# "run: /var/service/sshd: (pid 812) 3600s; run: log: ..."
line=$(sv status "$service_dir/$name" 2>/dev/null || true)
state=${line%%:*}
[[ $line =~ \(pid\ ([0-9]+)\) ]] && pid=${BASH_REMATCH[1]}
[[ $line =~ \ ([0-9]+)s ]] && time=$(human_time "${BASH_REMATCH[1]}")
[[ -n $state ]] || state='?'
fi
row "$name" "$enabled" "$state" "$pid" "$time"
done
}
# human_time SECONDS: 3d4h, 2h5m, 7m, 42s
human_time() {
local s=$1
if ((s >= 86400)); then
echo "$((s / 86400))d$((s % 86400 / 3600))h"
elif ((s >= 3600)); then
echo "$((s / 3600))h$((s % 3600 / 60))m"
elif ((s >= 60)); then
echo "$((s / 60))m"
else
echo "${s}s"
fi
}
cmd_status() {
local name rc=0
for name in "$@"; do
check_service "$name"
if is_enabled "$name"; then
sv status "$service_dir/$name" || rc=1
else
say "$name: вимкнена (dsv enable $name)" "$name: disabled (dsv enable $name)"
fi
done
return $rc
}
cmd_enable() {
local name
for name in "$@"; do
check_service "$name"
if is_enabled "$name"; then
say "$name: уже ввімкнена" "$name: already enabled"
continue
fi
ln -s "$sv_dir/$name" "$runlevel/$name"
say "$name: увімкнена" "$name: enabled"
# runsvdir starts it within 5 s. Right after "dsv disable" its runsv is
# still there with the service stopped, so ask for "up" as well.
if [[ $service_dir == /var/service ]] && wait_supervised "$name"; then
sv -v up "$service_dir/$name" || true
fi
done
}
cmd_disable() {
local name
for name in "$@"; do
check_service "$name"
if ! is_enabled "$name"; then
say "$name: уже вимкнена" "$name: already disabled"
continue
fi
# Stop it first: runsv only notices the removed link at runsvdir's next scan
[[ $service_dir == /var/service ]] && sv down "$service_dir/$name" 2>/dev/null || true
rm "$runlevel/$name"
say "$name: вимкнена" "$name: disabled"
done
}
cmd_control() {
local action=$1 name rc=0
shift
for name in "$@"; do
check_service "$name"
if ! is_enabled "$name"; then
say "$name: вимкнена, спершу: dsv enable $name" "$name: disabled, first: dsv enable $name" >&2
rc=1
continue
fi
# -v waits for the new state (up to 7 s) and prints it
sv -v "$action" "$service_dir/$name" || rc=1
done
return $rc
}
# The log directory of a service whose log/run starts svlogd, if any
svlogd_dir() {
local run=$sv_dir/$1/log/run words w
[[ -r $run ]] || return 1
grep -q svlogd "$run" || return 1
read -ra words < <(grep -m1 svlogd "$run")
for w in "${words[@]}"; do
[[ $w == /* && -d $w ]] && echo "$w" && return 0
done
return 1
}
cmd_logs() {
local name=${1:-} follow="" dir
[[ -n $name ]] || die "вкажіть службу: dsv logs СЛУЖБА [-f]" "name a service: dsv logs SERVICE [-f]"
[[ ${2:-} == -f ]] && follow=1
check_service "$name"
if dir=$(svlogd_dir "$name"); then
if [[ -n $follow ]]; then
exec tail -n 50 -F "$dir/current"
fi
cat "$dir"/@*.[su] "$dir/current" 2>/dev/null | tail -n 200
return
fi
# Services without their own log write to syslog (vlogger -t <name>, or the
# daemon itself as "<name>[pid]:" or "<name>-<helper>[pid]:", like
# sshd-session), which socklog sorts into directories by facility.
if ! compgen -G "$socklog/*/current" >/dev/null; then
die "у $name немає власного журналу, а syslog не записується; увімкніть socklog: dsv enable socklog-unix nanoklogd" \
"$name has no log of its own and syslog is not recorded; enable socklog: dsv enable socklog-unix nanoklogd"
fi
local pattern="[[:space:]]${name}(-[a-z]+)?(\[[0-9]+\])?:"
if [[ -n $follow ]]; then
tail -q -n 0 -F "$socklog"/*/current | grep --line-buffered -E "$pattern"
else
# A message can be in several directories (e.g. "everything" and "secure")
cat "$socklog"/*/@*.[su] "$socklog"/*/current 2>/dev/null | grep -E "$pattern" | sort -u | tail -n 200 ||
say "$name: у syslog немає записів (служба може писати лише в консоль runit)" \
"$name: no syslog entries (the service may only write to the runit console)"
fi
}
# check_running NAME: after a start, tells whether the service stays up or
# keeps restarting (runsv restarts a program that exits, once a second)
check_running() {
local line up='^run:[^;]*\) ([0-9]+)s'
sleep 3
line=$(sv status "$service_dir/$1" 2>/dev/null || true)
if [[ $line =~ $up ]] && ((BASH_REMATCH[1] >= 2)); then
say "$1: працює" "$1: running"
else
warn "$1 не працює стабільно (${line%%;*}); див. dsv logs $1" \
"$1 does not stay up (${line%%;*}); see dsv logs $1"
fi
}
cmd_new() {
local name="" run_cmd="" user="" dir="" log=1 enable="" envs=() e home bin
while (($#)); do
case $1 in
--cmd) run_cmd=${2:?}; shift ;;
--user) user=${2:?}; shift ;;
--dir) dir=${2:?}; shift ;;
--env) envs+=("${2:?}"); shift ;;
--no-log) log="" ;;
--enable) enable=1 ;;
-*) die "невідомий параметр $1 (dsv help)" "unknown option $1 (dsv help)" ;;
*) [[ -z $name ]] || die "зайвий аргумент $1" "extra argument $1"; name=$1 ;;
esac
shift
done
if [[ -z $name ]]; then
interactive || die "вкажіть назву служби" "name the service"
ask name "Назва служби" "Service name"
fi
[[ $name =~ ^[A-Za-z0-9][A-Za-z0-9_.@-]*$ ]] ||
die "назва '$name': лише латинські літери, цифри, '.', '_', '-', '@'" \
"name '$name': only Latin letters, digits, '.', '_', '-', '@'"
[[ ! -e $sv_dir/$name ]] || die "служба $name уже є" "service $name already exists"
if [[ -z $run_cmd ]]; then
interactive || die "вкажіть команду: --cmd" "give the command: --cmd"
say "Команда запуску. Програма має працювати на передньому плані (без -d/--daemon)." \
"The command to run. The program must stay in the foreground (no -d/--daemon)."
ask run_cmd "Команда" "Command"
[[ -n $run_cmd ]] || die "команду не вказано" "no command given"
fi
read -r bin _ <<<"$run_cmd"
if [[ $bin == */* ]]; then
[[ -x $bin ]] || die "$bin не існує або не виконуваний" "$bin does not exist or is not executable"
elif ! command -v "$bin" >/dev/null; then
die "команду $bin не знайдено; вкажіть повний шлях" "command $bin not found; give the full path"
fi
if [[ -z $user ]] && interactive; then
ask user "Від імені користувача" "Run as user" "${SUDO_USER:-root}"
fi
user=${user:-root}
id -u "$user" >/dev/null 2>&1 || die "немає користувача $user" "no user $user"
home=$(getent passwd "$user" | cut -d: -f6)
if [[ -z $dir ]] && interactive; then
ask dir "Робочий каталог" "Working directory" "$home"
fi
dir=${dir:-$home}
[[ -d $dir ]] || die "немає каталогу $dir" "no directory $dir"
if ((${#envs[@]} == 0)) && interactive; then
ask e "Змінні середовища ІМ'Я=ЗНАЧЕННЯ через пробіл (Enter — без них)" \
"Environment variables NAME=VALUE separated by spaces (Enter for none)"
read -ra envs <<<"$e"
fi
for e in "${envs[@]}"; do
[[ $e =~ ^[A-Za-z_][A-Za-z0-9_]*= ]] || die "'$e' не має вигляду ІМ'Я=ЗНАЧЕННЯ" "'$e' is not NAME=VALUE"
done
# Build it aside, then move it in place in one step
local tmp=$sv_dir/.$name.new
rm -rf "$tmp"
mkdir -p "$tmp/env"
# chpst does not set these for the new user
printf '%s\n' "$home" >"$tmp/env/HOME"
printf '%s\n' "$user" >"$tmp/env/USER"
for e in "${envs[@]}"; do
printf '%s\n' "${e#*=}" >"$tmp/env/${e%%=*}"
done
cat >"$tmp/run" <<EOF
#!/bin/sh
# Created by dsv new. After editing: dsv restart $name
# Environment variables: one file per variable in $sv_dir/$name/env
exec 2>&1
cd '$dir' || exit 1
exec chpst -u $user -e $sv_dir/$name/env $run_cmd
EOF
chmod 755 "$tmp/run"
if [[ -n $log ]]; then
mkdir -p "$tmp/log" "/var/log/$name"
cat >"$tmp/log/run" <<EOF
#!/bin/sh
# Log of $name, read with: dsv logs $name
mkdir -p /var/log/$name
exec svlogd -tt /var/log/$name
EOF
chmod 755 "$tmp/log/run"
fi
mv "$tmp" "$sv_dir/$name"
say "$name: створена в $sv_dir/$name" "$name: created in $sv_dir/$name"
if [[ -z $enable ]] && interactive; then
confirm "Увімкнути й запустити зараз?" "Enable and start it now?" && enable=1
fi
if [[ -n $enable ]]; then
cmd_enable "$name"
[[ $service_dir == /var/service ]] && check_running "$name"
else
say "Запустити: dsv enable $name" "To start it: dsv enable $name"
fi
}
cmd_rm() {
local name=${1:-} i
[[ -n $name ]] || die "вкажіть службу" "name a service"
check_service "$name"
# Services from packages come back with the next update; disable them instead
# (xbps-query -o succeeds even when no package owns the file)
if [[ -n $(xbps-query -o "$sv_dir/$name/run" 2>/dev/null) ]]; then
die "$name встановлена пакетом, її можна лише вимкнути: dsv disable $name" \
"$name comes from a package, it can only be disabled: dsv disable $name"
fi
if interactive && ! confirm "Видалити службу $name?" "Delete service $name?"; then
return 1
fi
if is_enabled "$name"; then
cmd_disable "$name"
# runsvdir stops its runsv at the next scan; wait for it before deleting
for ((i = 0; i < 20; i++)); do
pgrep -x -f "runsv $name" >/dev/null || break
sleep 0.5
done
fi
rm -rf "${sv_dir:?}/$name"
say "$name: видалена" "$name: deleted"
[[ -d /var/log/$name ]] && say "Журнал залишився в /var/log/$name" "The log is kept in /var/log/$name"
return 0
}
cmd=${1:-list}
[[ $# -gt 0 ]] && shift
case $cmd in
list | ls)
need_root list "$@"
cmd_list
;;
status)
[[ $# -gt 0 ]] || die "вкажіть службу" "name a service"
need_root status "$@"
cmd_status "$@"
;;
enable | disable | start | stop | restart)
[[ $# -gt 0 ]] || die "вкажіть службу" "name a service"
need_root "$cmd" "$@"
case $cmd in
enable) cmd_enable "$@" ;;
disable) cmd_disable "$@" ;;
start) cmd_control up "$@" ;;
stop) cmd_control down "$@" ;;
restart) cmd_control restart "$@" ;;
esac
;;
logs | log)
# socklog's logs are readable by root and the socklog group only
[[ -r $socklog/messages/current ]] || need_root logs "$@"
cmd_logs "$@"
;;
new | create)
need_root new "$@"
cmd_new "$@"
;;
rm | remove | delete)
need_root rm "$@"
cmd_rm "$@"
;;
-h | --help | help)
usage
;;
*)
usage >&2
exit 1
;;
esac
@@ -0,0 +1,41 @@
# bash completion for dsv
# shellcheck shell=bash
_dsv() {
local cur=${COMP_WORDS[COMP_CWORD]} cmd=${COMP_WORDS[1]:-}
if ((COMP_CWORD == 1)); then
mapfile -t COMPREPLY < <(compgen -W "list status enable disable start stop restart logs new rm help" -- "$cur")
return
fi
case $cmd in
enable)
# services that are not enabled yet
local all=() s
for s in /etc/sv/*/; do
s=${s%/}
s=${s##*/}
[[ -e /etc/runit/runsvdir/default/$s ]] || all+=("$s")
done
mapfile -t COMPREPLY < <(compgen -W "${all[*]}" -- "$cur")
;;
disable | start | stop | restart)
mapfile -t COMPREPLY < <(compgen -W "$(ls /etc/runit/runsvdir/default 2>/dev/null)" -- "$cur")
;;
new)
case ${COMP_WORDS[COMP_CWORD - 1]} in
--user) mapfile -t COMPREPLY < <(compgen -u -- "$cur") ;;
--dir) mapfile -t COMPREPLY < <(compgen -d -- "$cur") ;;
--cmd) mapfile -t COMPREPLY < <(compgen -c -- "$cur") ;;
--env) ;;
*) mapfile -t COMPREPLY < <(compgen -W "--cmd --user --dir --env --no-log --enable" -- "$cur") ;;
esac
;;
status | logs | log | rm)
if [[ $cmd == logs* ]] && ((COMP_CWORD == 3)); then
mapfile -t COMPREPLY < <(compgen -W "-f" -- "$cur")
else
mapfile -t COMPREPLY < <(compgen -W "$(ls /etc/sv 2>/dev/null)" -- "$cur")
fi
;;
esac
}
complete -F _dsv dsv
+247
View File
@@ -0,0 +1,247 @@
#!/bin/bash
# dsys - system settings on Das OS: host name, time zone, language and
# console keyboard, in place of hostnamectl, timedatectl and localectl.
set -euo pipefail
# shellcheck source=helpers.sh
. "${DAS_HELPERS:-/usr/lib/das/helpers.sh}"
zoneinfo=/usr/share/zoneinfo
libc_locales=/etc/default/libc-locales
keymaps=/usr/share/kbd/keymaps
usage() {
if [[ -n $uk ]]; then
cat <<'EOF'
Використання: dsys [КОМАНДА] [ЗНАЧЕННЯ]
info огляд системи (типова команда)
hostname [ІМ'Я] показати або змінити ім'я хоста
timezone [ЗОНА] показати або змінити часовий пояс (Europe/Kyiv,
достатньо й частини назви: Kyiv)
timezone list [ФІЛЬТР] усі часові пояси
locale [МОВА] показати або змінити мову системи (uk_UA, en_US)
locale list [ФІЛЬТР] доступні мови; * — уже зібрані
keymap [РОЗКЛАДКА] показати або змінити розкладку консолі (us, ua-utf)
keymap list [ФІЛЬТР] доступні розкладки консолі
Мову й розкладку графічного сеансу кожен користувач задає в налаштуваннях
Xfce і в das-welcome; dsys змінює типові для системи й консолі.
EOF
else
cat <<'EOF'
Usage: dsys [COMMAND] [VALUE]
info system overview (the default)
hostname [NAME] show or change the host name
timezone [ZONE] show or change the time zone (Europe/Kyiv,
part of the name will do: Kyiv)
timezone list [FILTER] all time zones
locale [LANGUAGE] show or change the system language (uk_UA, en_US)
locale list [FILTER] available languages; * marks the built ones
keymap [MAP] show or change the console keyboard map (us, ua-utf)
keymap list [FILTER] available console keyboard maps
In the graphical session every user picks the language and keyboard in the
Xfce settings and in das-welcome; dsys changes the system and console defaults.
EOF
fi
}
current_hostname() {
cat /etc/hostname 2>/dev/null || hostname
}
current_timezone() {
local tz
tz=$(readlink /etc/localtime 2>/dev/null || true)
echo "${tz#*zoneinfo/}"
}
current_locale() {
sed -n 's/^LANG=//p' /etc/locale.conf 2>/dev/null | tr -d '"'
}
rc_value() {
sed -n "s/^$1=//p" /etc/rc.conf 2>/dev/null | tr -d '"' | tail -n 1
}
# A graphical session keeps its X authority under the old host name
session_warning() {
if pgrep -x lightdm >/dev/null || [[ -n ${DISPLAY:-} ]]; then
say "Перезавантажте комп'ютер: графічний сеанс не відкриватиме нових вікон зі старим іменем." \
"Reboot: the graphical session will not open new windows under the old name."
fi
}
# 3 д 4 год 5 хв / 3 d 4 h 5 min
uptime_text() {
local s d h m
read -r s _ </proc/uptime
s=${s%.*}
d=$((s / 86400))
h=$((s % 86400 / 3600))
m=$((s % 3600 / 60))
if ((d > 0)); then
say "$d д $h год $m хв" "$d d $h h $m min"
elif ((h > 0)); then
say "$h год $m хв" "$h h $m min"
else
say "$m хв" "$m min"
fi
}
cmd_info() {
local ntp line
pad "$(say "Ім'я хоста:" "Host name:")" 16; current_hostname
pad "$(say "Система:" "System:")" 16; echo "$(uname -sr), $(uname -m)"
pad "$(say "Працює:" "Up:")" 16; uptime_text
pad "$(say "Час:" "Time:")" 16; date '+%Y-%m-%d %H:%M:%S %Z'
pad "$(say "Часовий пояс:" "Time zone:")" 16; current_timezone
if ntp=$(chronyc -n tracking 2>/dev/null); then
if grep -q '^Leap status *: Normal' <<<"$ntp"; then
ntp=$(say "синхронізовано (chrony)" "synchronized (chrony)")
else
ntp=$(say "не синхронізовано (chrony)" "not synchronized (chrony)")
fi
else
ntp=$(say "не працює (dsv enable chronyd)" "not running (dsv enable chronyd)")
fi
pad "$(say "Синхронізація:" "Time sync:")" 16; echo "$ntp"
pad "$(say "Мова:" "Language:")" 16; current_locale
pad "$(say "Консоль:" "Console:")" 16
echo "$(say розкладка keymap) $(rc_value KEYMAP || true), $(say шрифт font) $(rc_value FONT || true)"
say "Мережа:" "Network:"
ip -brief address 2>/dev/null | while read -r line; do
[[ $line == lo\ * ]] || echo " $line"
done
}
cmd_hostname() {
local new=${1:-} old
if [[ -z $new ]]; then
current_hostname
return
fi
[[ $new =~ ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$ ]] ||
die "'$new': малі латинські літери, цифри й дефіс (не на початку й не в кінці)" \
"'$new': lowercase Latin letters, digits and hyphens (not first or last)"
need_root hostname "$new"
old=$(current_hostname)
echo "$new" >/etc/hostname
# The installers add "127.0.1.1 <name>"; keep the line in step
if grep -qw -- "$old" /etc/hosts; then
sed -i "/^[^#]/s/\([[:space:]]\)${old//./\\.}\([[:space:]]\|$\)/\1$new\2/g" /etc/hosts
fi
grep -qw -- "$new" /etc/hosts || printf '127.0.1.1\t%s\n' "$new" >>/etc/hosts
hostname "$new"
say "Ім'я хоста: $new" "Host name: $new"
session_warning
}
# All time zones, from tzdata's table plus UTC
timezones() {
{ awk '!/^#/ { print $3 }' "$zoneinfo/zone1970.tab"; echo UTC; } | sort -u
}
cmd_timezone() {
local zone=${1:-} matches line
if [[ -z $zone ]]; then
current_timezone
return
fi
if [[ $zone == list ]]; then
timezones | grep -i -- "${2:-}" || true
return
fi
if ! timezones | grep -qx -- "$zone"; then
# Kyiv -> Europe/Kyiv, when only one zone matches
matches=$(timezones | grep -i -- "$zone" || true)
if [[ $(grep -c . <<<"$matches") -eq 1 ]]; then
zone=$matches
elif [[ -n $matches ]]; then
say "Кілька часових поясів підходять, уточніть:" "Several time zones match, be more specific:" >&2
head -n 20 <<<"$matches" | while read -r line; do echo " $line"; done >&2
(($(grep -c . <<<"$matches") <= 20)) || echo " ..." >&2
exit 1
else
die "невідомий часовий пояс '$zone' (dsys timezone list)" "unknown time zone '$zone' (dsys timezone list)"
fi
fi
need_root timezone "$zone"
ln -sfn "$zoneinfo/$zone" /etc/localtime
say "Часовий пояс: $zone, зараз $(date '+%H:%M %Z')" "Time zone: $zone, now $(date '+%H:%M %Z')"
}
# Every UTF-8 locale glibc can build, with * for the enabled ones
locales() {
sed -n 's/^\(#\?\)\([^ ]*\.UTF-8\) UTF-8[[:space:]]*$/\2 \1/p' "$libc_locales" |
awk '{ print ($2 == "#" ? " " : "* ") $1 }'
}
cmd_locale() {
local lang=${1:-} re
if [[ -z $lang ]]; then
current_locale
return
fi
if [[ $lang == list ]]; then
locales | grep -i -- "${2:-}" || true
return
fi
[[ $lang == *.* ]] || lang+=.UTF-8
re=${lang//./\\.}
locales | grep -q "^. $re\$" ||
die "невідома мова '$lang' (dsys locale list)" "unknown language '$lang' (dsys locale list)"
need_root locale "$lang"
if grep -q "^#$re " "$libc_locales"; then
sed -i "s/^#\($re \)/\1/" "$libc_locales"
say "Збираю $lang..." "Building $lang..."
xbps-reconfigure -f glibc-locales >/dev/null
fi
# Keep the other settings, Void's default file also has LC_COLLATE=C
if grep -q '^LANG=' /etc/locale.conf 2>/dev/null; then
sed -i "s/^LANG=.*/LANG=$lang/" /etc/locale.conf
else
echo "LANG=$lang" >>/etc/locale.conf
fi
say "Мова системи: $lang (діє з наступного входу)" "System language: $lang (from the next login)"
}
cmd_keymap() {
local map=${1:-}
if [[ -z $map ]]; then
rc_value KEYMAP
return
fi
if [[ $map == list ]]; then
find "$keymaps" -name '*.map.gz' -printf '%f\n' | sed 's/\.map\.gz$//' | sort -u | grep -i -- "${2:-}" || true
return
fi
[[ -n $(find "$keymaps" -name "$map.map.gz" -print -quit) ]] ||
die "невідома розкладка '$map' (dsys keymap list)" "unknown keyboard map '$map' (dsys keymap list)"
need_root keymap "$map"
set_rc KEYMAP "$map"
# The default console font has no Cyrillic letters
if [[ $map == @(ua|ru|by|bg|mk|sr)* && -z $(rc_value FONT) ]]; then
set_rc FONT cyr-sun16
fi
loadkeys "$map" >/dev/null 2>&1 || true
say "Розкладка консолі: $map" "Console keyboard map: $map"
}
cmd=${1:-info}
[[ $# -gt 0 ]] && shift
case $cmd in
info | status) cmd_info ;;
hostname | host) cmd_hostname "$@" ;;
timezone | tz) cmd_timezone "$@" ;;
locale | lang) cmd_locale "$@" ;;
keymap | keyboard) cmd_keymap "$@" ;;
-h | --help | help) usage ;;
*)
usage >&2
exit 1
;;
esac
@@ -0,0 +1,22 @@
# bash completion for dsys
# shellcheck shell=bash
_dsys() {
local cur=${COMP_WORDS[COMP_CWORD]}
if ((COMP_CWORD == 1)); then
mapfile -t COMPREPLY < <(compgen -W "info hostname timezone locale keymap help" -- "$cur")
return
fi
((COMP_CWORD == 2)) || return
case ${COMP_WORDS[1]} in
timezone | tz)
mapfile -t COMPREPLY < <(compgen -W "list UTC $(awk '!/^#/ { print $3 }' /usr/share/zoneinfo/zone1970.tab 2>/dev/null)" -- "$cur")
;;
locale | lang)
mapfile -t COMPREPLY < <(compgen -W "list $(sed -n 's/^#\?\([^ ]*\)\.UTF-8 UTF-8[[:space:]]*$/\1/p' /etc/default/libc-locales 2>/dev/null)" -- "$cur")
;;
keymap | keyboard)
mapfile -t COMPREPLY < <(compgen -W "list $(find /usr/share/kbd/keymaps -name '*.map.gz' -printf '%f\n' 2>/dev/null | sed 's/\.map\.gz$//')" -- "$cur")
;;
esac
}
complete -F _dsys dsys
+396
View File
@@ -0,0 +1,396 @@
#!/bin/bash
# duser - manage user accounts on Das OS: create them with the right groups,
# set passwords, lock them, add SSH keys.
set -euo pipefail
# shellcheck source=helpers.sh
. "${DAS_HELPERS:-/usr/lib/das/helpers.sh}"
# Groups of every new user, as the desktop installer gives them; only the ones
# that exist on this system are used
user_groups="users audio video input storage network optical scanner lp kvm plugdev bluetooth"
# Administrators also get sudo, Docker (root-equivalent) and the system logs
admin_groups="wheel docker socklog"
uid_min=$(awk '$1 == "UID_MIN" { print $2 }' /etc/login.defs 2>/dev/null)
uid_max=$(awk '$1 == "UID_MAX" { print $2 }' /etc/login.defs 2>/dev/null)
uid_min=${uid_min:-1000}
uid_max=${uid_max:-60000}
usage() {
if [[ -n $uk ]]; then
cat <<'EOF'
Використання: duser КОМАНДА [КОРИСТУВАЧ] [...]
list користувачі: адміністратор, вхід, SSH-ключі, групи
add [КОРИСТУВАЧ] створити; чого не вказано, те запитає:
--admin адміністратор (sudo, docker, журнали)
--key КЛЮЧ|ФАЙЛ публічний SSH-ключ
--no-password без пароля, вхід лише за ключем
--password-stdin пароль зі стандартного входу (для скриптів)
del КОРИСТУВАЧ [--home] видалити (--home: разом із домашнім каталогом)
passwd [КОРИСТУВАЧ] змінити пароль (без імені — свій)
lock КОРИСТУВАЧ заборонити вхід (і за паролем, і за ключем)
unlock КОРИСТУВАЧ дозволити вхід знову
key КОРИСТУВАЧ [list] SSH-ключі користувача
key КОРИСТУВАЧ add [КЛЮЧ|ФАЙЛ|-]
додати ключ (рядок, файл .pub або stdin)
key КОРИСТУВАЧ rm НОМЕР видалити ключ за номером зі списку
groups КОРИСТУВАЧ [+ГРУПА|-ГРУПА...]
показати групи, додати (+) чи прибрати (-);
+wheel робить адміністратором
Зміни груп діють із наступного входу користувача.
EOF
else
cat <<'EOF'
Usage: duser COMMAND [USER] [...]
list users: administrator, login, SSH keys, groups
add [USER] create one; asks for what is not given:
--admin an administrator (sudo, docker, system logs)
--key KEY|FILE a public SSH key
--no-password no password, key login only
--password-stdin read the password from stdin (for scripts)
del USER [--home] delete (--home: with the home directory)
passwd [USER] change the password (without a name: your own)
lock USER forbid logins (by password and by key)
unlock USER allow logins again
key USER [list] the user's SSH keys
key USER add [KEY|FILE|-]
add a key (a line, a .pub file or stdin)
key USER rm NUMBER remove a key by its number in the list
groups USER [+GROUP|-GROUP...]
show the groups, add (+) or remove (-) some;
+wheel makes an administrator
Group changes apply from the user's next login.
EOF
fi
}
# A regular (human) account, not a system one
check_user() {
local uid
uid=$(id -u "$1" 2>/dev/null) || die "немає користувача $1" "no user $1"
((uid >= uid_min && uid <= uid_max)) ||
die "$1 — системний обліковий запис, duser його не змінює" "$1 is a system account, duser leaves it alone"
}
in_group() {
id -nG "$1" | tr ' ' '\n' | grep -qx "$2"
}
# existing GROUPS...: the given groups that exist, comma-separated
existing() {
local g out=()
for g in "$@"; do
getent group "$g" >/dev/null && out+=("$g")
done
local IFS=,
echo "${out[*]}"
}
# login_state USER: how the user can log in, from /etc/shadow
login_state() {
local hash expire today
IFS=: read -r _ hash _ _ _ _ _ expire _ < <(getent shadow "$1")
today=$(($(date +%s) / 86400))
if [[ -n $expire ]] && ((expire <= today)); then
say заблоковано locked
elif [[ $hash == \$* ]]; then
say пароль password
else
# "!", "*" or a "!"-locked hash: SSH keys still work
say "лише ключ" "key only"
fi
}
keys_file() {
echo "$(getent passwd "$1" | cut -d: -f6)/.ssh/authorized_keys"
}
# The keys of a file, one per line, without comments and blank lines
key_lines() {
[[ -r $1 ]] || return 0
grep -v '^[[:space:]]*\(#\|$\)' "$1" || true
}
cmd_list() {
local name uid home admin keys
pad "$(say КОРИСТУВАЧ USER)" 16
pad UID 6
pad "$(say АДМІН ADMIN)" 6
pad "$(say ВХІД LOGIN)" 12
pad "$(say КЛЮЧІ KEYS)" 6
say ГРУПИ GROUPS
while IFS=: read -r name _ uid _ _ home _; do
((uid >= uid_min && uid <= uid_max)) || continue
admin=$(say ні no)
in_group "$name" wheel && admin=$(say так yes)
keys=$(key_lines "$home/.ssh/authorized_keys" | wc -l)
pad "$name" 16
pad "$uid" 6
pad "$admin" 6
pad "$(login_state "$name")" 12
pad "$keys" 6
id -nG "$name" | sed "s/^$name //; s/ /,/g"
done < <(getent passwd | sort -t: -k3n)
}
# add_key USER KEY|FILE|-: appends a validated key to the user's authorized_keys
add_key() {
local user=$1 src=$2 key file tmp group
if [[ $src == - ]]; then
key=$(cat)
elif [[ -f $src ]]; then
key=$(cat "$src")
else
key=$src
fi
key=$(printf '%s\n' "$key" | grep -v '^[[:space:]]*$' | head -n 1)
tmp=$(mktemp)
printf '%s\n' "$key" >"$tmp"
if ! ssh-keygen -l -f "$tmp" >/dev/null 2>&1; then
rm -f "$tmp"
die "це не публічний SSH-ключ (потрібен рядок на кшталт 'ssh-ed25519 AAAA... ім'я')" \
"this is not a public SSH key (expected a line like 'ssh-ed25519 AAAA... name')"
fi
rm -f "$tmp"
file=$(keys_file "$user")
# Same key type and body, whatever the comment
if key_lines "$file" | awk -v k="$(awk '{ print $1, $2 }' <<<"$key")" '$1 " " $2 == k { found = 1 } END { exit !found }'; then
say "$user: цей ключ уже є" "$user: this key is already there"
return 0
fi
group=$(id -gn "$user")
install -d -m 700 -o "$user" -g "$group" "$(dirname "$file")"
printf '%s\n' "$key" >>"$file"
chown "$user:$group" "$file"
chmod 600 "$file"
say "$user: ключ додано" "$user: key added"
}
# set_password USER: asks on a terminal, reads one line from stdin otherwise
set_password() {
local password
if interactive; then
passwd "$1"
else
IFS= read -r password || die "пароль не передано" "no password given"
[[ -n $password ]] || die "порожній пароль" "empty password"
# -c: Void's PAM stack for chpasswd does not hash passwords itself
printf '%s:%s\n' "$1" "$password" | chpasswd -c SHA512
say "$1: пароль змінено" "$1: password changed"
fi
}
cmd_add() {
local name="" admin="" key="" password=ask groups
while (($#)); do
case $1 in
--admin) admin=1 ;;
--key) key=${2:?}; shift ;;
--no-password) password=none ;;
--password-stdin) password=stdin ;;
-*) die "невідомий параметр $1 (duser help)" "unknown option $1 (duser help)" ;;
*) [[ -z $name ]] || die "зайвий аргумент $1" "extra argument $1"; name=$1 ;;
esac
shift
done
if [[ -z $name ]]; then
interactive || die "вкажіть ім'я користувача" "name the user"
ask name "Ім'я користувача (латиницею)" "User name"
fi
[[ $name =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] ||
die "ім'я '$name': малі латинські літери, цифри, '_', '-', до 32 символів" \
"name '$name': lowercase Latin letters, digits, '_', '-', up to 32 characters"
! id "$name" >/dev/null 2>&1 || die "користувач $name уже є" "user $name already exists"
if [[ -z $admin ]] && interactive && [[ $password != stdin ]]; then
confirm "Адміністратор (sudo, docker, журнали)?" "Administrator (sudo, docker, system logs)?" && admin=1
fi
if [[ -z $key ]] && interactive && [[ $password != stdin ]]; then
ask key "Публічний SSH-ключ: рядок або файл .pub (Enter — без ключа)" \
"Public SSH key: a line or a .pub file (Enter for none)"
fi
[[ $password == ask ]] && ! interactive && password=none
# shellcheck disable=SC2086 # word lists
groups=$(existing $user_groups ${admin:+$admin_groups})
useradd -m -s /bin/bash -G "$groups" "$name"
say "$name: створено, групи $groups" "$name: created, groups $groups"
# From here on the account exists: on a failure, say so instead of leaving
# it half set up silently
# shellcheck disable=SC2064 # $name is fixed by now
trap "[[ \$? -eq 0 ]] || warn '$name створено не повністю; виправте через duser або видаліть: duser del $name --home' \
'$name is only partly set up; fix it with duser or delete it: duser del $name --home'" EXIT
[[ -n $key ]] && add_key "$name" "$key"
case $password in
ask | stdin) set_password "$name" ;;
none)
[[ -n $key ]] || warn "$name не має ні пароля, ні ключа і поки не зможе увійти: duser passwd $name" \
"$name has neither a password nor a key and cannot log in yet: duser passwd $name"
;;
esac
trap - EXIT
}
# The current user (the one who ran sudo)
me() {
echo "${SUDO_USER:-$(id -un)}"
}
cmd_del() {
local name=${1:-} home="" others
[[ -n $name ]] || die "вкажіть користувача" "name the user"
[[ ${2:-} == --home ]] && home=1
check_user "$name"
[[ $name != "$(me)" ]] || die "не можна видалити себе" "you cannot delete yourself"
if in_group "$name" wheel; then
others=$(getent group wheel | cut -d: -f4 | tr ',' '\n' | grep -vx "$name" || true)
[[ -n $others ]] || die "$name — єдиний адміністратор" "$name is the only administrator"
fi
if interactive; then
confirm "Видалити користувача $name?" "Delete user $name?" || return 1
if [[ -z $home ]] && confirm "Видалити й домашній каталог $(getent passwd "$name" | cut -d: -f6)?" \
"Delete the home directory $(getent passwd "$name" | cut -d: -f6) too?"; then
home=1
fi
fi
# userdel refuses while the user has running processes, which is right.
# Void creates no mail spools, so drop userdel's warning about it.
local out
if ! out=$(userdel ${home:+-r} "$name" 2>&1); then
echo "$out" >&2
exit 1
fi
grep -v 'mail spool' <<<"$out" >&2 || true
say "$name: видалено" "$name: deleted"
}
cmd_passwd() {
local name=${1:-}
if [[ -z $name || $name == "$(id -un)" ]] && [[ $EUID -ne 0 ]]; then
# Your own password: no sudo, passwd asks for the current one
exec passwd
fi
need_root passwd "$@"
name=${name:-$(me)}
check_user "$name"
set_password "$name"
}
cmd_lock() {
local name=${1:-}
[[ -n $name ]] || die "вкажіть користувача" "name the user"
check_user "$name"
[[ $name != "$(me)" ]] || die "не можна заблокувати себе" "you cannot lock yourself out"
# -L disables the password; an expired account also stops SSH key logins
usermod -L -e 1 "$name"
say "$name: вхід заборонено (поточні сеанси тривають; завершити: pkill -KILL -u $name)" \
"$name: logins forbidden (open sessions go on; to end them: pkill -KILL -u $name)"
}
cmd_unlock() {
local name=${1:-} hash
[[ -n $name ]] || die "вкажіть користувача" "name the user"
check_user "$name"
# (usermod -e '' complains "no changes" when the account never expired)
chage -E -1 "$name"
# Re-enable the password only when there is one under the "!"
hash=$(getent shadow "$name" | cut -d: -f2)
[[ $hash == \!\$* ]] && usermod -U "$name"
say "$name: вхід дозволено" "$name: logins allowed"
}
cmd_key() {
local name=${1:-} action=${2:-list} file n i line
[[ -n $name ]] || die "вкажіть користувача" "name the user"
check_user "$name"
file=$(keys_file "$name")
case $action in
list | ls)
if [[ -z $(key_lines "$file") ]]; then
say "$name: SSH-ключів немає" "$name: no SSH keys"
return 0
fi
i=0
while IFS= read -r line; do
i=$((i + 1))
printf '%2d %s\n' "$i" "$(ssh-keygen -l -f - <<<"$line" 2>/dev/null || echo "? $line")"
done < <(key_lines "$file")
;;
add)
local src=${3:-}
if [[ -z $src ]]; then
interactive || die "вкажіть ключ, файл або -" "give a key, a file or -"
ask src "Публічний SSH-ключ: рядок або файл .pub" "Public SSH key: a line or a .pub file"
fi
add_key "$name" "$src"
;;
rm | del)
n=${3:-}
[[ $n =~ ^[1-9][0-9]*$ ]] || die "вкажіть номер ключа (duser key $name)" "give the key number (duser key $name)"
line=$(key_lines "$file" | sed -n "${n}p")
[[ -n $line ]] || die "немає ключа номер $n" "no key number $n"
# Remove that exact line, keeping comments and the other keys
awk -v k="$line" '$0 != k' "$file" >"$file.new"
chown --reference="$file" "$file.new"
chmod 600 "$file.new"
mv "$file.new" "$file"
say "$name: ключ $n видалено" "$name: key $n removed"
;;
*) die "невідома дія $action (list, add, rm)" "unknown action $action (list, add, rm)" ;;
esac
}
cmd_groups() {
local name=${1:-} arg g
[[ -n $name ]] || die "вкажіть користувача" "name the user"
check_user "$name"
shift
for arg in "$@"; do
g=${arg:1}
case $arg in
+?*)
getent group "$g" >/dev/null || die "немає групи $g" "no group $g"
gpasswd -a "$name" "$g" >/dev/null
;;
-?*)
if [[ $g == wheel && $name == "$(me)" ]]; then
die "не можна позбавити себе прав адміністратора" "you cannot drop your own administrator rights"
fi
gpasswd -d "$name" "$g" >/dev/null
;;
*) die "'$arg': потрібно +ГРУПА або -ГРУПА" "'$arg': expected +GROUP or -GROUP" ;;
esac
done
echo "$name: $(id -nG "$name" | sed "s/^$name //; s/ /, /g")"
}
cmd=${1:-list}
[[ $# -gt 0 ]] && shift
case $cmd in
list | ls)
# /etc/shadow is root-only
need_root list
cmd_list
;;
add | new) need_root add "$@"; cmd_add "$@" ;;
del | rm | remove) need_root del "$@"; cmd_del "$@" ;;
passwd | password) cmd_passwd "$@" ;;
lock) need_root lock "$@"; cmd_lock "$@" ;;
unlock) need_root unlock "$@"; cmd_unlock "$@" ;;
key | keys) need_root key "$@"; cmd_key "$@" ;;
groups | group) need_root groups "$@"; cmd_groups "$@" ;;
-h | --help | help) usage ;;
*)
usage >&2
exit 1
;;
esac
@@ -0,0 +1,42 @@
# bash completion for duser
# shellcheck shell=bash
_duser_users() {
local min max
min=$(awk '$1 == "UID_MIN" { print $2 }' /etc/login.defs 2>/dev/null)
max=$(awk '$1 == "UID_MAX" { print $2 }' /etc/login.defs 2>/dev/null)
getent passwd | awk -F: -v min="${min:-1000}" -v max="${max:-60000}" '$3 >= min && $3 <= max { print $1 }'
}
_duser() {
local cur=${COMP_WORDS[COMP_CWORD]} cmd=${COMP_WORDS[1]:-}
if ((COMP_CWORD == 1)); then
mapfile -t COMPREPLY < <(compgen -W "list add del passwd lock unlock key groups help" -- "$cur")
return
fi
case $cmd in
add)
if [[ ${COMP_WORDS[COMP_CWORD - 1]} == --key ]]; then
mapfile -t COMPREPLY < <(compgen -f -- "$cur")
else
mapfile -t COMPREPLY < <(compgen -W "--admin --key --no-password --password-stdin" -- "$cur")
fi
;;
del | passwd | lock | unlock | key | groups)
if ((COMP_CWORD == 2)); then
mapfile -t COMPREPLY < <(compgen -W "$(_duser_users)" -- "$cur")
elif [[ $cmd == del ]]; then
mapfile -t COMPREPLY < <(compgen -W "--home" -- "$cur")
elif [[ $cmd == key ]] && ((COMP_CWORD == 3)); then
mapfile -t COMPREPLY < <(compgen -W "list add rm" -- "$cur")
elif [[ $cmd == key && ${COMP_WORDS[3]} == add ]]; then
mapfile -t COMPREPLY < <(compgen -f -- "$cur")
elif [[ $cmd == groups ]]; then
# +GROUP and -GROUP
local sign=${cur:0:1}
[[ $sign == [+-] ]] || sign=+
mapfile -t COMPREPLY < <(compgen -P "$sign" -W "$(getent group | cut -d: -f1)" -- "${cur#[+-]}")
fi
;;
esac
}
complete -F _duser duser
+75
View File
@@ -0,0 +1,75 @@
# Shared functions of the Das OS helpers (dsv, duser, dfw, dsys).
# Sourced, not run: each helper sets "set -euo pipefail" itself.
# shellcheck shell=bash
self=${0##*/}
if [[ ${LC_ALL:-} == uk* || ${LC_MESSAGES:-} == uk* || ${LANG:-} == uk* ]]; then
uk=1
else
uk=""
fi
# say UKRAINIAN ENGLISH: prints the message in the user's language
say() {
if [[ -n $uk ]]; then echo "$1"; else echo "$2"; fi
}
warn() {
say "$self: $1" "$self: $2" >&2
}
die() {
warn "$1" "$2"
exit 1
}
# need_root ARGS...: runs the helper again through sudo with the same arguments
need_root() {
if [[ $EUID -ne 0 ]]; then
exec sudo "$0" "$@"
fi
}
# Questions are only asked on a terminal; scripts must pass every argument
interactive() {
[[ -t 0 ]]
}
# ask VAR UKRAINIAN ENGLISH [DEFAULT]: reads a line into VAR; the prompt shows
# the default, which an empty answer keeps
ask() {
local -n _answer=$1
local prompt
prompt=$(say "$2" "$3")
if [[ -n ${4:-} ]]; then
prompt+=" [$4]"
fi
read -r -e -p "$prompt: " _answer || exit 1
[[ -n $_answer ]] || _answer=${4:-}
}
# confirm UKRAINIAN ENGLISH: a yes/no question, "no" by default
confirm() {
local reply
read -r -p "$(say "$1 [т/Н]" "$2 [y/N]") " reply || return 1
[[ ${reply,,} == @(y|yes|т|так) ]]
}
# pad TEXT WIDTH: TEXT padded with spaces to WIDTH characters. printf pads by
# bytes, ${#s} counts characters, so Cyrillic columns stay aligned.
pad() {
local width=$(($2 - ${#1}))
((width > 0)) || width=0
printf '%s%*s ' "$1" "$width" ''
}
# set_rc VAR VALUE: sets VAR in /etc/rc.conf, uncommenting Void's example line
set_rc() {
local rc=${RC_CONF:-/etc/rc.conf}
if grep -q "^#\?$1=" "$rc"; then
sed -i "s|^#\?$1=.*|$1=\"$2\"|" "$rc"
else
echo "$1=\"$2\"" >>"$rc"
fi
}
+21
View File
@@ -0,0 +1,21 @@
# Template file for 'das-helpers'
pkgname=das-helpers
version=0.1.0
revision=1
depends="bash runit sudo shadow openssh nftables kbd"
short_desc="Das OS helpers: services (dsv), users (duser), firewall (dfw), system (dsys)"
maintainer="Das <anry.das@gmail.com>"
license="MIT"
homepage="https://git.das.org.ua/das/Das-OS"
# dsv used to be a package of its own
replaces="dsv>=0"
provides="dsv-${version}_${revision}"
do_install() {
vinstall ${FILESDIR}/helpers.sh 644 usr/lib/das
for tool in dsv duser dfw dsys; do
vbin ${FILESDIR}/${tool}
vcompletion ${FILESDIR}/${tool}.bash-completion bash ${tool}
done
vlicense ${FILESDIR}/LICENSE
}
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Das
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+17
View File
@@ -0,0 +1,17 @@
#!/bin/sh
# Mount the clean live image for Calamares' unpackfs at /run/das-installer/root.
#
# Dracut runs the live root as a device-mapper snapshot (live-rw) that also
# holds the live user and boot-time tweaks. Its origin, live-base, is the
# untouched LiveOS/rootfs.img; mount it read-only without journal replay.
set -eu
root=/run/das-installer/root
mountpoint -q "$root" && exit 0
if [ ! -b /dev/mapper/live-base ]; then
echo "mount-source: /dev/mapper/live-base not found, is this the live system?" >&2
exit 1
fi
mkdir -p "$root"
mount -o ro,noload /dev/mapper/live-base "$root"
+74
View File
@@ -0,0 +1,74 @@
#!/bin/sh
# Finish a Das OS installation. The installers (Calamares, das-server-install)
# run this inside the target system after users and locale are configured,
# before GRUB.
set -eu
msg() { echo "das-postinstall: $*"; }
# Calamares writes LANG to /etc/locale.conf, but glibc on Void only builds the
# locales enabled in /etc/default/libc-locales.
lang=$(sed -n 's/^LANG=//p' /etc/locale.conf 2>/dev/null | tr -d '"')
for l in "$lang" en_US.UTF-8; do
[ -n "$l" ] || continue
re=$(printf '%s' "$l" | sed 's/[.]/\\./g')
sed -i "s/^#\(${re} \)/\1/" /etc/default/libc-locales
done
msg "building locales"
xbps-reconfigure -f glibc-locales
# Calamares mounts /tmp on tmpfs only when the root disk is an SSD; Void's own
# installer always does.
if ! grep -q '^[^#]*[[:space:]]/tmp[[:space:]]' /etc/fstab; then
echo "tmpfs /tmp tmpfs defaults,nosuid,nodev 0 0" >>/etc/fstab
fi
# mklive's list of packages left out of the image; das-server keeps its own
rm -f /etc/xbps.d/mklive-ignore.conf
# Guest tools only help inside their own hypervisor (VirtualBox's ones even
# complain at every login elsewhere).
machine=$(cat /sys/class/dmi/id/sys_vendor /sys/class/dmi/id/product_name 2>/dev/null || true)
if ! grep -q '^flags.* hypervisor' /proc/cpuinfo; then
machine="real hardware"
fi
case $machine in
*VirtualBox* | *innotek*) unused="spice-vdagent qemu-ga" ;;
*QEMU* | *KVM*) unused="virtualbox-ose-guest" ;;
*) unused="spice-vdagent qemu-ga virtualbox-ose-guest" ;;
esac
installed=""
for pkg in $unused; do
xbps-query "$pkg" >/dev/null 2>&1 && installed="$installed $pkg"
done
case $installed in
*spice-vdagent*) rm -f /etc/runit/runsvdir/default/spice-vdagentd ;;
esac
case $installed in
# The live image replaces this autostart file, so xbps keeps it as modified
*virtualbox*) rm -f /etc/xdg/autostart/vboxclient.desktop ;;
esac
if [ -n "$installed" ]; then
msg "removing guest tools not needed here:$installed"
# shellcheck disable=SC2086
xbps-remove -Ry $installed || true
fi
# Snapper, hourly snapshots and the GRUB snapshot menu (Btrfs only).
das-snapper-setup
# The installer copies files with rsync --sparse, which turns the zero tail of
# a kernel image into a Btrfs hole that GRUB 2.12 can't read ("premature end
# of file"). Rewrite the kernels in full.
for f in /boot/vmlinuz-*; do
cp --sparse=never "$f" "$f.das-tmp"
mv "$f.das-tmp" "$f"
done
# The live image has a generic initramfs. Build the target's own the same way
# Void does on kernel updates (host-only), with das-snapshot-overlay included.
msg "building the initramfs"
for kver in $(ls /usr/lib/modules); do
dracut -q --force "/boot/initramfs-$kver.img" "$kver"
done
msg "done"
+15
View File
@@ -0,0 +1,15 @@
# Template file for 'das-install-common'
pkgname=das-install-common
version=0.2.0
revision=1
depends="das-snapshots dracut rsync"
short_desc="Das OS installer steps shared by the desktop and server installers"
maintainer="Das <anry.das@gmail.com>"
license="MIT"
homepage="https://git.das.org.ua/das/Das-OS"
do_install() {
vinstall ${FILESDIR}/mount-source 755 usr/libexec/das/installer
vinstall ${FILESDIR}/postinstall 755 usr/libexec/das/installer
vlicense ${FILESDIR}/LICENSE
}
+1
View File
@@ -0,0 +1 @@
../../../LICENSE
@@ -0,0 +1,37 @@
---
componentName: dasos
welcomeStyleCalamares: false
welcomeExpandingLogo: true
windowExpanding: normal
windowSize: 960px,640px
windowPlacement: center
sidebar: widget
navigation: widget
strings:
productName: "Das OS"
shortProductName: "Das OS"
version: "@VERSION@"
shortVersion: "@SHORT_VERSION@"
versionedName: "Das OS @VERSION@"
shortVersionedName: "Das OS @SHORT_VERSION@"
bootloaderEntryName: "Das OS"
productUrl: "https://git.das.org.ua/das/Das-OS"
supportUrl: "https://git.das.org.ua/das/Das-OS/issues"
images:
productIcon: "/usr/share/icons/hicolor/scalable/apps/das-logo.svg"
productLogo: "/usr/share/icons/hicolor/scalable/apps/das-logo.svg"
productWelcome: "welcome.svg"
slideshow:
- "slide-1.svg"
- "slide-2.svg"
- "slide-3.svg"
style:
SidebarBackground: "#1b1f27"
SidebarText: "#e3e6ec"
SidebarTextCurrent: "#1b1f27"
SidebarBackgroundCurrent: "#ffd23f"
@@ -0,0 +1,28 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="800" height="440" viewBox="0 0 800 440">
<defs>
<path id="petal" d="M0 0 L19.13 -46.19 L0 -92.39 L-19.13 -46.19 Z"/>
<path id="core" d="M0 -25.4 L8.61 -46.19 L0 -66.98 L-8.61 -46.19 Z"/>
<g id="pb"><use xlink:href="#petal" fill="#2f6fde"/><use xlink:href="#core" fill="#ffd23f"/></g>
<g id="py"><use xlink:href="#petal" fill="#f5c400"/><use xlink:href="#core" fill="#1f4fa8"/></g>
<g id="star">
<use xlink:href="#pb"/><use xlink:href="#py" transform="rotate(45)"/>
<use xlink:href="#pb" transform="rotate(90)"/><use xlink:href="#py" transform="rotate(135)"/>
<use xlink:href="#pb" transform="rotate(180)"/><use xlink:href="#py" transform="rotate(225)"/>
<use xlink:href="#pb" transform="rotate(270)"/><use xlink:href="#py" transform="rotate(315)"/>
</g>
<pattern id="stitch" patternUnits="userSpaceOnUse" width="28" height="20">
<path d="M14 0 L24 10 L14 20 L4 10 Z" fill="#2f6fde"/>
<path d="M14 6 L18 10 L14 14 L10 10 Z" fill="#f5c400"/>
</pattern>
</defs>
<rect width="800" height="440" fill="#f6f7fa"/>
<rect y="420" width="800" height="20" fill="url(#stitch)"/>
<use xlink:href="#star" transform="translate(150 210) scale(1.15)"/>
<g font-family="Inter, sans-serif" fill="#1f2430">
<text x="300" y="150" font-size="34" font-weight="600">Ласкаво просимо!</text>
<text x="300" y="205" font-size="19">Das OS — легка система на Void Linux</text>
<text x="300" y="235" font-size="19">з робочим столом Xfce, українською</text>
<text x="300" y="265" font-size="19">мовою та звичними програмами.</text>
<text x="300" y="295" font-size="19"></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,28 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="800" height="440" viewBox="0 0 800 440">
<defs>
<path id="petal" d="M0 0 L19.13 -46.19 L0 -92.39 L-19.13 -46.19 Z"/>
<path id="core" d="M0 -25.4 L8.61 -46.19 L0 -66.98 L-8.61 -46.19 Z"/>
<g id="pb"><use xlink:href="#petal" fill="#2f6fde"/><use xlink:href="#core" fill="#ffd23f"/></g>
<g id="py"><use xlink:href="#petal" fill="#f5c400"/><use xlink:href="#core" fill="#1f4fa8"/></g>
<g id="star">
<use xlink:href="#pb"/><use xlink:href="#py" transform="rotate(45)"/>
<use xlink:href="#pb" transform="rotate(90)"/><use xlink:href="#py" transform="rotate(135)"/>
<use xlink:href="#pb" transform="rotate(180)"/><use xlink:href="#py" transform="rotate(225)"/>
<use xlink:href="#pb" transform="rotate(270)"/><use xlink:href="#py" transform="rotate(315)"/>
</g>
<pattern id="stitch" patternUnits="userSpaceOnUse" width="28" height="20">
<path d="M14 0 L24 10 L14 20 L4 10 Z" fill="#2f6fde"/>
<path d="M14 6 L18 10 L14 14 L10 10 Z" fill="#f5c400"/>
</pattern>
</defs>
<rect width="800" height="440" fill="#f6f7fa"/>
<rect y="420" width="800" height="20" fill="url(#stitch)"/>
<use xlink:href="#star" transform="translate(150 210) scale(1.15)"/>
<g font-family="Inter, sans-serif" fill="#1f2430">
<text x="300" y="150" font-size="34" font-weight="600">Знімки системи</text>
<text x="300" y="205" font-size="19">Btrfs і snapper щогодини зберігають</text>
<text x="300" y="235" font-size="19">стан системи. Попередній знімок можна</text>
<text x="300" y="265" font-size="19">завантажити з меню GRUB і повернути</text>
<text x="300" y="295" font-size="19">командою sudo snapper-rollback.</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,28 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="800" height="440" viewBox="0 0 800 440">
<defs>
<path id="petal" d="M0 0 L19.13 -46.19 L0 -92.39 L-19.13 -46.19 Z"/>
<path id="core" d="M0 -25.4 L8.61 -46.19 L0 -66.98 L-8.61 -46.19 Z"/>
<g id="pb"><use xlink:href="#petal" fill="#2f6fde"/><use xlink:href="#core" fill="#ffd23f"/></g>
<g id="py"><use xlink:href="#petal" fill="#f5c400"/><use xlink:href="#core" fill="#1f4fa8"/></g>
<g id="star">
<use xlink:href="#pb"/><use xlink:href="#py" transform="rotate(45)"/>
<use xlink:href="#pb" transform="rotate(90)"/><use xlink:href="#py" transform="rotate(135)"/>
<use xlink:href="#pb" transform="rotate(180)"/><use xlink:href="#py" transform="rotate(225)"/>
<use xlink:href="#pb" transform="rotate(270)"/><use xlink:href="#py" transform="rotate(315)"/>
</g>
<pattern id="stitch" patternUnits="userSpaceOnUse" width="28" height="20">
<path d="M14 0 L24 10 L14 20 L4 10 Z" fill="#2f6fde"/>
<path d="M14 6 L18 10 L14 14 L10 10 Z" fill="#f5c400"/>
</pattern>
</defs>
<rect width="800" height="440" fill="#f6f7fa"/>
<rect y="420" width="800" height="20" fill="url(#stitch)"/>
<use xlink:href="#star" transform="translate(150 210) scale(1.15)"/>
<g font-family="Inter, sans-serif" fill="#1f2430">
<text x="300" y="150" font-size="34" font-weight="600">Ваші файли в безпеці</text>
<text x="300" y="205" font-size="19">Домашня тека лежить в окремому</text>
<text x="300" y="235" font-size="19">підтомі й не входить до знімків,</text>
<text x="300" y="265" font-size="19">тому відкат системи її не зачіпає.</text>
<text x="300" y="295" font-size="19"></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="480" height="300" viewBox="0 0 480 300">
<defs>
<path id="petal" d="M0 0 L19.13 -46.19 L0 -92.39 L-19.13 -46.19 Z"/>
<path id="core" d="M0 -25.4 L8.61 -46.19 L0 -66.98 L-8.61 -46.19 Z"/>
<g id="pb"><use xlink:href="#petal" fill="#2f6fde"/><use xlink:href="#core" fill="#ffd23f"/></g>
<g id="py"><use xlink:href="#petal" fill="#f5c400"/><use xlink:href="#core" fill="#1f4fa8"/></g>
<g id="star">
<use xlink:href="#pb"/><use xlink:href="#py" transform="rotate(45)"/>
<use xlink:href="#pb" transform="rotate(90)"/><use xlink:href="#py" transform="rotate(135)"/>
<use xlink:href="#pb" transform="rotate(180)"/><use xlink:href="#py" transform="rotate(225)"/>
<use xlink:href="#pb" transform="rotate(270)"/><use xlink:href="#py" transform="rotate(315)"/>
</g>
<pattern id="stitch" patternUnits="userSpaceOnUse" width="28" height="20">
<path d="M14 0 L24 10 L14 20 L4 10 Z" fill="#2f6fde"/>
<path d="M14 6 L18 10 L14 14 L10 10 Z" fill="#f5c400"/>
</pattern>
</defs>
<use xlink:href="#star" transform="translate(240 130) scale(1.15)"/>
<text x="240" y="285" text-anchor="middle" font-family="Inter, sans-serif" font-size="34" font-weight="600" fill="#2f6fde">Das OS</text>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,13 @@
---
efiBootLoader: "grub"
kernelSearchPath: "/boot"
kernelPattern: "^vmlinuz.*"
kernelParams: [ "quiet", "loglevel=3" ]
grubInstall: "grub-install"
grubMkconfig: "grub-mkconfig"
grubCfg: "/boot/grub/grub.cfg"
grubProbe: "grub-probe"
efiBootMgr: "efibootmgr"
efiBootloaderId: "dasos"
installEFIFallback: true
installHybridGRUB: false
@@ -0,0 +1,10 @@
---
displaymanagers:
- lightdm
defaultDesktopEnvironment:
executable: "startxfce4"
desktopFile: "xfce"
basicSetup: false
sysconfigSetup: false
lightdm:
preferred_greeters: ["lightdm-gtk-greeter.desktop"]
@@ -0,0 +1,4 @@
---
restartNowMode: user-unchecked
restartNowCommand: "loginctl reboot"
notifyOnFinished: false
@@ -0,0 +1,9 @@
---
crypttabOptions: luks
tmpOptions:
default:
tmpfs: true
options: "defaults,nosuid,nodev,mode=1777"
ssd:
tmpfs: true
options: "defaults,noatime,nosuid,nodev,mode=1777"
@@ -0,0 +1,13 @@
---
overwrite: false
prefer_grub_d: false
keep_distributor: false
kernel_params: [ "quiet", "loglevel=3" ]
defaults:
GRUB_TIMEOUT: 5
GRUB_DEFAULT: "saved"
GRUB_DISABLE_SUBMENU: true
GRUB_TERMINAL_OUTPUT: "console"
GRUB_DISABLE_RECOVERY: true
GRUB_DISABLE_OS_PROBER: false
always_use_defaults: false
@@ -0,0 +1,7 @@
---
xOrgConfFileName: "/etc/X11/xorg.conf.d/00-keyboard.conf"
convertedKeymapPath: "/usr/share/kbd/keymaps/xkb"
writeEtcDefaultKeyboard: false
configure:
kwin: false
gnome: false
@@ -0,0 +1,5 @@
---
region: "Europe"
zone: "Kyiv"
geoip:
style: none
@@ -0,0 +1,6 @@
# Void has no systemd: only D-Bus needs a machine id (/var/lib/dbus/machine-id).
---
systemd: false
dbus: true
dbus-symlink: false
entropy-copy: false
@@ -0,0 +1,48 @@
---
extraMounts:
- device: proc
fs: proc
mountPoint: /proc
- device: sys
fs: sysfs
mountPoint: /sys
- device: /dev
mountPoint: /dev
options: [ bind ]
- device: tmpfs
fs: tmpfs
mountPoint: /run
- device: /run/udev
mountPoint: /run/udev
options: [ bind ]
- device: efivarfs
fs: efivarfs
mountPoint: /sys/firmware/efi/efivars
efi: true
# The flat layout that snapper-rollback expects: the root in @ and snapshots
# in @snapshots. Home, logs and the package cache stay out of root snapshots,
# so a rollback never touches personal files.
btrfsSubvolumes:
- mountPoint: /
subvolume: /@
- mountPoint: /home
subvolume: /@home
- mountPoint: /.snapshots
subvolume: /@snapshots
- mountPoint: /var/log
subvolume: /@var_log
- mountPoint: /var/cache/xbps
subvolume: /@var_cache_xbps
mountOptions:
- filesystem: default
options: [ defaults ]
- filesystem: efi
options: [ defaults, umask=0077 ]
- filesystem: btrfs
options: [ defaults, noatime, compress=zstd:1 ]
- filesystem: ext4
options: [ defaults, noatime ]
- filesystem: xfs
options: [ defaults, noatime ]
@@ -0,0 +1,11 @@
# The target is a copy of the live system: drop the installer and whatever was
# installed only for it.
---
backend: xbps
skip_if_no_internet: false
update_db: false
update_system: false
operations:
- try_remove:
- das-installer
- das-install-common
@@ -0,0 +1,29 @@
---
efi:
mountPoint: "/boot/efi"
recommendedSize: 512MiB
minimumSize: 64MiB
label: "EFI"
userSwapChoices:
- none
- small
- suspend
initialSwapChoice: none
drawNestedPartitions: false
alwaysShowPartitionLabels: true
allowManualPartitioning: true
initialPartitioningChoice: none
# Btrfs subvolumes (see mount.conf) give snapper snapshots and rollback.
defaultFileSystemType: "btrfs"
availableFileSystemTypes: [ "btrfs", "ext4", "xfs" ]
# Disk encryption needs extra initramfs and GRUB setup that Das OS does not
# have yet.
enableLuksAutomatedPartitioning: false
essentialMounts: [ "live-*", "control", "ventoy" ]
lvm:
enable: false
@@ -0,0 +1,7 @@
---
dontChroot: false
timeout: 900
verbose: true
script:
- command: "/usr/libexec/das/installer/postinstall"
timeout: 900
@@ -0,0 +1,9 @@
# The first snapshot is the freshly installed system. grub-mkconfig then adds
# it to the boot menu through grub-btrfs.
---
dontChroot: false
timeout: 300
verbose: true
script:
- "-snapper --no-dbus -c root create -d 'Fresh install' -c number --userdata important=yes"
- "-grub-mkconfig -o /boot/grub/grub.cfg"
@@ -0,0 +1,5 @@
---
dontChroot: true
timeout: 60
script:
- "/usr/libexec/das/installer/mount-source"
@@ -0,0 +1,7 @@
# Copy the clean live image that shellprocess@source mounts, not the running
# live root (see mount-source).
---
unpack:
- source: "/run/das-installer/root"
sourcefs: "file"
destination: ""
@@ -0,0 +1,53 @@
---
defaultGroups:
- name: users
must_exist: true
system: true
- name: wheel
must_exist: true
system: true
- audio
- video
- input
- storage
- network
- optical
- scanner
- lp
- kvm
- plugdev
- name: bluetooth
must_exist: false
system: true
sudoersGroup: wheel
sudoersConfigureWithGroup: true
autologinGroup: autologin
setRootPassword: true
doReusePassword: true
displayAutologin: true
doAutologin: false
passwordRequirements:
minLength: 4
maxLength: -1
allowWeakPasswords: true
allowWeakPasswordsDefault: true
user:
shell: /bin/bash
forbidden_names: [ root, live ]
home_permissions: "o700"
hostname:
location: EtcFile
writeHostsFile: true
template: "${login}-dasos"
forbidden_names: [ localhost ]
presets:
fullName:
editable: true
loginName:
editable: true
@@ -0,0 +1,24 @@
---
showSupportUrl: true
showKnownIssuesUrl: false
showReleaseNotesUrl: false
showDonateUrl: false
requirements:
requiredStorage: 15
requiredRam: 1.5
internetCheckUrl: https://voidlinux.org
check:
- storage
- ram
- power
- internet
- root
- screen
required:
- storage
- ram
- root
geoip:
style: none
@@ -0,0 +1,54 @@
# Calamares settings for Das OS (Void Linux, runit).
---
modules-search: [ local ]
instances:
- id: source
module: shellprocess
config: shellprocess-source.conf
- id: postinstall
module: shellprocess
config: shellprocess-postinstall.conf
- id: snapshot
module: shellprocess
config: shellprocess-snapshot.conf
sequence:
- show:
- welcome
- locale
- keyboard
- partition
- users
- summary
- exec:
- partition
- mount
- shellprocess@source
- unpackfs
- machineid
- locale
- keyboard
- localecfg
- fstab
- users
- displaymanager
- networkcfg
- hwclock
- grubcfg
- shellprocess@postinstall
- packages
- bootloader
- shellprocess@snapshot
- umount
- show:
- finished
branding: dasos
prompt-install: true
dont-chroot: false
oem-setup: false
disable-cancel: false
disable-cancel-during-exec: false
hide-back-and-next-during-exec: true
quit-at-end: false
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
# Start the Das OS installer as root. The live user has passwordless sudo.
exec sudo --preserve-env=DISPLAY,XAUTHORITY,LANG,LANGUAGE,LC_ALL \
/usr/bin/calamares "$@"
@@ -0,0 +1,7 @@
[Desktop Entry]
Type=Application
Name=Das OS installer icon
Comment=Put the installer on the live desktop
Exec=/usr/libexec/das/installer/installer-icon
OnlyShowIn=XFCE;
NoDisplay=true
@@ -0,0 +1,12 @@
[Desktop Entry]
Type=Application
Name=Install Das OS
Name[uk]=Встановити Das OS
Comment=Install Das OS on this computer
Comment[uk]=Встановити Das OS на цей комп'ютер
Exec=das-install
Icon=das-logo
Terminal=false
Categories=System;
Keywords=installer;calamares;
Keywords[uk]=встановлення;інсталятор;calamares;
+12
View File
@@ -0,0 +1,12 @@
#!/bin/sh
# Put the installer launcher on the live user's desktop. Xfce runs launchers
# from the desktop only when they are executable and marked as trusted.
desktop=$(xdg-user-dir DESKTOP 2>/dev/null)
[ -n "$desktop" ] && [ "$desktop" != "$HOME" ] || desktop="$HOME/Desktop"
launcher="$desktop/das-installer.desktop"
[ -e "$launcher" ] && exit 0
mkdir -p "$desktop"
install -m 755 /usr/share/applications/das-installer.desktop "$launcher"
gio set -t string "$launcher" metadata::xfce-exe-checksum \
"$(sha256sum "$launcher" | cut -d' ' -f1)" 2>/dev/null || true
+20
View File
@@ -0,0 +1,20 @@
# Template file for 'das-installer'
pkgname=das-installer
version=0.3.0
revision=1
depends="calamares das-install-common das-theme sudo"
short_desc="Das OS installer: Calamares configuration and branding for the live ISO"
maintainer="Das <anry.das@gmail.com>"
license="MIT"
homepage="https://git.das.org.ua/das/Das-OS"
do_install() {
local f
vmkdir usr/share/calamares
vcopy "${FILESDIR}/calamares/*" usr/share/calamares
vbin ${FILESDIR}/das-install
vinstall ${FILESDIR}/installer-icon 755 usr/libexec/das/installer
vinstall ${FILESDIR}/das-installer.desktop 644 usr/share/applications
vinstall ${FILESDIR}/das-installer-icon.desktop 644 etc/xdg/autostart
vlicense ${FILESDIR}/LICENSE
}
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Das
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+582
View File
@@ -0,0 +1,582 @@
#!/bin/bash
# das-server-install - console installer for Das OS Server.
#
# Usage: das-server-install [--config FILE]
#
# Asks its questions with dialog and installs the live image on a disk:
# GPT with an EFI or BIOS boot partition and Btrfs with the same subvolumes
# as the desktop installer, so snapshots and rollback work the same way.
# With --config it asks nothing and takes the answers from FILE (see
# /usr/share/examples/das-server-installer/install.conf).
set -Eeuo pipefail
log=/tmp/das-server-install.log
target=/mnt/das-target
source_root=/run/das-installer/root
libexec=/usr/libexec/das/installer
# Answers; a config file sets the same variables
DISK=""
HOST_NAME=dasos-server
USER_NAME=""
USER_PASSWORD=""
SSH_KEY=""
SSH_PASSWORDS=yes
TIMEZONE=Europe/Kyiv
KEYMAP=us
LOCALE=en_US.UTF-8
NETWORK=dhcpcd
ERASE_DISK=no
lang=en
[[ ${LANG:-} == uk* ]] && lang=uk
# t KEY: the message in the installer's language
t() {
local key=$1
if [[ $lang == uk ]]; then
case $key in
title) echo "Встановлення Das OS Server" ;;
lang) echo "Мова встановлення:" ;;
disk) echo "Диск для встановлення. Усі дані на ньому буде знищено." ;;
nodisk) echo "Не знайдено дисків для встановлення." ;;
host) echo "Ім'я сервера (латинські літери, цифри, дефіс):" ;;
badhost) echo "Неправильне ім'я сервера." ;;
user) echo "Ім'я користувача-адміністратора (вхід через sudo, root вимкнено):" ;;
baduser) echo "Неправильне ім'я: малі латинські літери, цифри, _ та -, не root." ;;
pass) echo "Пароль користувача:" ;;
pass2) echo "Ще раз той самий пароль:" ;;
badpass) echo "Паролі порожні або не збігаються." ;;
key) echo "Публічний ключ SSH для входу (вставте рядок ssh-ed25519 ... або залиште порожнім):" ;;
badkey) echo "Це не схоже на публічний ключ SSH." ;;
nopass) echo "Заборонити вхід по SSH за паролем (лише за ключем)?" ;;
tz) echo "Часовий пояс (наприклад, Europe/Kyiv, UTC):" ;;
badtz) echo "Немає такого часового поясу." ;;
keymap) echo "Розкладка клавіатури в консолі:" ;;
locale) echo "Мова системи:" ;;
net) echo "Мережа:" ;;
net_dhcpcd) echo "dhcpcd: DHCP на всіх інтерфейсах (просто)" ;;
net_nm) echo "NetworkManager: налаштування через nmtui (Wi-Fi, статичні адреси)" ;;
summary) echo "Перевірте налаштування:" ;;
erase) echo "УСІ ДАНІ НА ДИСКУ %s БУДЕ ЗНИЩЕНО. Продовжити?" ;;
cancel) echo "Встановлення скасовано." ;;
s_source) echo "Підготовка образу системи..." ;;
s_disk) echo "Розмітка диска %s..." ;;
s_copy) echo "Копіювання системи (кілька хвилин)..." ;;
s_config) echo "Налаштування системи..." ;;
s_post) echo "Локалі, знімки, initramfs (кілька хвилин)..." ;;
s_boot) echo "Встановлення завантажувача..." ;;
finished) echo "Das OS Server встановлено. Вийміть інсталяційний носій і перезавантажте сервер.\n\nВхід: %s, далі sudo. Служби: dsv. Користувачі: duser. Порти: dfw. Налаштування: dsys. Оновлення: das-update." ;;
reboot) echo "Перезавантажити зараз?" ;;
failed) echo "Помилка встановлення. Журнал: %s" ;;
root) echo "Інсталятор треба запускати від root (sudo das-server-install)." ;;
yes) echo "так" ;;
no) echo "ні" ;;
esac
else
case $key in
title) echo "Das OS Server installation" ;;
lang) echo "Installation language:" ;;
disk) echo "Disk to install to. All data on it will be erased." ;;
nodisk) echo "No disks to install to were found." ;;
host) echo "Server name (Latin letters, digits, hyphen):" ;;
badhost) echo "Invalid server name." ;;
user) echo "Administrator user name (uses sudo, root login is disabled):" ;;
baduser) echo "Invalid name: lowercase Latin letters, digits, _ and -, not root." ;;
pass) echo "User password:" ;;
pass2) echo "The same password again:" ;;
badpass) echo "The passwords are empty or do not match." ;;
key) echo "SSH public key to log in with (paste the ssh-ed25519 ... line, or leave empty):" ;;
badkey) echo "This does not look like an SSH public key." ;;
nopass) echo "Disable SSH password login (keys only)?" ;;
tz) echo "Time zone (for example Europe/Kyiv, UTC):" ;;
badtz) echo "No such time zone." ;;
keymap) echo "Console keyboard layout:" ;;
locale) echo "System language:" ;;
net) echo "Network:" ;;
net_dhcpcd) echo "dhcpcd: DHCP on all interfaces (simple)" ;;
net_nm) echo "NetworkManager: set up with nmtui (Wi-Fi, static addresses)" ;;
summary) echo "Check the settings:" ;;
erase) echo "ALL DATA ON %s WILL BE ERASED. Continue?" ;;
cancel) echo "Installation cancelled." ;;
s_source) echo "Preparing the system image..." ;;
s_disk) echo "Partitioning %s..." ;;
s_copy) echo "Copying the system (a few minutes)..." ;;
s_config) echo "Configuring the system..." ;;
s_post) echo "Locales, snapshots, initramfs (a few minutes)..." ;;
s_boot) echo "Installing the boot loader..." ;;
finished) echo "Das OS Server is installed. Remove the installation medium and reboot.\n\nLog in as %s, then use sudo. Services: dsv. Users: duser. Ports: dfw. Settings: dsys. Updates: das-update." ;;
reboot) echo "Reboot now?" ;;
failed) echo "Installation failed. Log: %s" ;;
root) echo "The installer must run as root (sudo das-server-install)." ;;
yes) echo "yes" ;;
no) echo "no" ;;
esac
fi
}
interactive=1
dialog_ui() {
local labels=()
if [[ $lang == uk ]]; then
labels=(--ok-label "Гаразд" --cancel-label "Скасувати" --yes-label "Так" --no-label "Ні")
fi
dialog --backtitle "Das OS Server" --title "$(t title)" "${labels[@]}" "$@" 3>&1 1>&2 2>&3
}
# Shows a step in the dialog, or prints it when installing from a config file
step() {
if [[ -n $interactive ]]; then
dialog --backtitle "Das OS Server" --title "$(t title)" --infobox "$1" 5 70
echo "==> $1" >>"$log"
else
echo "==> $1" | tee -a "$log"
fi
}
fail() {
trap - ERR
local msg
# shellcheck disable=SC2059
msg=$(printf "$(t failed)" "$log")
if [[ -n $interactive ]]; then
dialog --backtitle "Das OS Server" --title "$(t title)" --msgbox "$msg\n\n$(tail -n 12 "$log" | sed 's/\\/\\\\/g')" 22 76 || true
clear
fi
echo "das-server-install: $msg" >&2
exit 1
}
cancelled() {
[[ -n $interactive ]] && clear
t cancel
exit 1
}
# --- Questions -------------------------------------------------------------
# Disks that can be installed to: not read-only, not the live medium
list_disks() {
local live="" dev
if dev=$(blkid -L VOID_LIVE 2>/dev/null); then
live=$(lsblk -no PKNAME "$dev" 2>/dev/null | head -n 1)
[[ -n $live ]] && live=/dev/$live || live=$dev
fi
lsblk -dpno NAME,TYPE,RO,SIZE,MODEL | while read -r name type ro size model; do
[[ $type == disk && $ro == 0 && $name != "$live" ]] || continue
[[ $name == /dev/zram* || $name == /dev/loop* ]] && continue
printf '%s\t%s %s\n' "$name" "$size" "${model:-}"
done
}
ask_language() {
local choice
choice=$(dialog --backtitle "Das OS Server" --title "Das OS Server" \
--default-item "$lang" --menu "Language / Мова:" 10 50 2 \
en "English" uk "Українська" 3>&1 1>&2 2>&3) || cancelled
lang=$choice
if [[ $lang == uk ]]; then
LOCALE=uk_UA.UTF-8
# The default console font has no Cyrillic (a font needs a Unicode table,
# .psfu); there is no font to set on serial or ssh
case $(tty) in /dev/tty[0-9]*) setfont cyr-sun16 2>/dev/null || true ;; esac
fi
}
ask_disk() {
local items=() name desc
while IFS=$'\t' read -r name desc; do
items+=("$name" "$desc")
done < <(list_disks)
if [[ ${#items[@]} -eq 0 ]]; then
dialog_ui --msgbox "$(t nodisk)" 7 60 || true
cancelled
fi
DISK=$(dialog_ui --menu "$(t disk)" 15 70 6 "${items[@]}") || cancelled
}
valid_host() {
[[ $1 =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$ ]]
}
valid_user() {
[[ $1 =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] || return 1
case $1 in root | live | nobody | daemon | bin | sys) return 1 ;; esac
}
valid_key() {
[[ -z $1 || $1 =~ ^(ssh-(ed25519|rsa|dss)|ecdsa-sha2-[a-z0-9]+|sk-[a-z0-9@.-]+)\ [A-Za-z0-9+/=]+ ]]
}
ask_host() {
while :; do
HOST_NAME=$(dialog_ui --inputbox "$(t host)" 8 60 "$HOST_NAME") || cancelled
valid_host "$HOST_NAME" && return
dialog_ui --msgbox "$(t badhost)" 6 50 || true
done
}
ask_user() {
local p1 p2
while :; do
USER_NAME=$(dialog_ui --inputbox "$(t user)" 9 60 "$USER_NAME") || cancelled
valid_user "$USER_NAME" && break
dialog_ui --msgbox "$(t baduser)" 7 60 || true
done
while :; do
p1=$(dialog_ui --insecure --passwordbox "$(t pass)" 8 60) || cancelled
p2=$(dialog_ui --insecure --passwordbox "$(t pass2)" 8 60) || cancelled
if [[ -n $p1 && $p1 == "$p2" ]]; then
USER_PASSWORD=$p1
return
fi
dialog_ui --msgbox "$(t badpass)" 6 50 || true
done
}
ask_ssh() {
while :; do
SSH_KEY=$(dialog_ui --inputbox "$(t key)" 10 76 "$SSH_KEY") || cancelled
valid_key "$SSH_KEY" && break
dialog_ui --msgbox "$(t badkey)" 6 50 || true
done
SSH_PASSWORDS=yes
if [[ -n $SSH_KEY ]] && dialog_ui --yesno "$(t nopass)" 7 60; then
SSH_PASSWORDS=no
fi
}
ask_system() {
while :; do
TIMEZONE=$(dialog_ui --inputbox "$(t tz)" 8 60 "$TIMEZONE") || cancelled
[[ -f /usr/share/zoneinfo/$TIMEZONE ]] && break
dialog_ui --msgbox "$(t badtz)" 6 50 || true
done
KEYMAP=$(dialog_ui --default-item "$KEYMAP" --menu "$(t keymap)" 15 50 7 \
us "English (US)" ua-utf "Українська" uk "English (UK)" de-latin1 "Deutsch" \
fr-latin1 "Français" pl2 "Polski") || cancelled
LOCALE=$(dialog_ui --default-item "$LOCALE" --menu "$(t locale)" 10 50 2 \
en_US.UTF-8 "English" uk_UA.UTF-8 "Українська") || cancelled
NETWORK=$(dialog_ui --default-item "$NETWORK" --menu "$(t net)" 10 76 2 \
dhcpcd "$(t net_dhcpcd)" NetworkManager "$(t net_nm)") || cancelled
}
confirm() {
local key pw msg
key=$(t no)
pw=$(t yes)
[[ -n $SSH_KEY ]] && key=$(t yes)
[[ $SSH_PASSWORDS == no ]] && pw=$(t no)
dialog_ui --no-collapse --msgbox "$(t summary)
Disk: $DISK
Host: $HOST_NAME
User: $USER_NAME
SSH key: $key
SSH passwords: $pw
Time zone: $TIMEZONE
Keyboard: $KEYMAP
Language: $LOCALE
Network: $NETWORK" 18 60 || cancelled
# shellcheck disable=SC2059
msg=$(printf "$(t erase)" "$DISK")
dialog_ui --defaultno --yesno "$msg" 7 60 || cancelled
ERASE_DISK=yes
}
# --- Installation ----------------------------------------------------------
# part N: the device of partition N on $DISK
part() {
lsblk -lnpo NAME,TYPE "$DISK" | awk '$2 == "part" { print $1 }' | sed -n "${1}p"
}
in_target() {
chroot "$target" "$@"
}
# Undo mounts from an earlier attempt or a failure
unmount_target() {
if mountpoint -q "$target"; then
umount -R "$target" || umount -Rl "$target"
fi
}
uefi() {
[[ -d /sys/firmware/efi ]]
}
serial_console() {
grep -q 'console=ttyS0' /proc/cmdline
}
partition() {
local efi root
unmount_target
wipefs -a "$DISK"
sgdisk --zap-all "$DISK"
if uefi; then
sgdisk -n 1:0:+512M -t 1:ef00 -c 1:EFI -n 2:0:0 -t 2:8300 -c 2:dasos "$DISK"
else
# GRUB on BIOS keeps its core image in a small partition on GPT
sgdisk -n 1:0:+1M -t 1:ef02 -c 1:bios -n 2:0:0 -t 2:8300 -c 2:dasos "$DISK"
fi
partprobe "$DISK" 2>/dev/null || true
udevadm settle
efi=$(part 1)
root=$(part 2)
[[ -b $root ]] || return 1
uefi && mkfs.vfat -F 32 -n EFI "$efi"
mkfs.btrfs -f -L dasos "$root"
# The same subvolumes as the Calamares configuration (mount.conf)
mkdir -p "$target"
mount "$root" "$target"
for sv in @ @home @snapshots @var_log @var_cache_xbps; do
btrfs subvolume create "$target/$sv"
done
umount "$target"
local opts=noatime,compress=zstd:1
mount -o "$opts,subvol=@" "$root" "$target"
mkdir -p "$target"/{home,.snapshots,var/log,var/cache/xbps}
mount -o "$opts,subvol=@home" "$root" "$target/home"
mount -o "$opts,subvol=@snapshots" "$root" "$target/.snapshots"
mount -o "$opts,subvol=@var_log" "$root" "$target/var/log"
mount -o "$opts,subvol=@var_cache_xbps" "$root" "$target/var/cache/xbps"
if uefi; then
mkdir -p "$target/boot/efi"
mount -o umask=0077 "$efi" "$target/boot/efi"
fi
}
copy_system() {
# No --sparse: GRUB can't read kernels with Btrfs holes (see postinstall)
rsync -aHAX --numeric-ids "$source_root/" "$target/"
}
write_fstab() {
local root_uuid efi_uuid opts=noatime,compress=zstd:1
root_uuid=$(blkid -s UUID -o value "$(part 2)")
{
echo "# <file system> <dir> <type> <options> <dump> <pass>"
echo "UUID=$root_uuid / btrfs $opts,subvol=@ 0 0"
echo "UUID=$root_uuid /home btrfs $opts,subvol=@home 0 0"
echo "UUID=$root_uuid /.snapshots btrfs $opts,subvol=@snapshots 0 0"
echo "UUID=$root_uuid /var/log btrfs $opts,subvol=@var_log 0 0"
echo "UUID=$root_uuid /var/cache/xbps btrfs $opts,subvol=@var_cache_xbps 0 0"
if uefi; then
efi_uuid=$(blkid -s UUID -o value "$(part 1)")
echo "UUID=$efi_uuid /boot/efi vfat defaults,umask=0077 0 2"
fi
} >"$target/etc/fstab"
}
mount_chroot() {
mount -t proc proc "$target/proc"
mount -t sysfs sys "$target/sys"
mount --rbind /dev "$target/dev"
mount -t tmpfs tmpfs "$target/run"
if uefi; then
mount -t efivarfs efivarfs "$target/sys/firmware/efi/efivars" 2>/dev/null || true
fi
}
# set_rc VAR VALUE: sets VAR in /etc/rc.conf, uncommenting Void's example line
set_rc() {
local rc=$target/etc/rc.conf
if grep -q "^#\?$1=" "$rc"; then
sed -i "s|^#\?$1=.*|$1=\"$2\"|" "$rc"
else
echo "$1=\"$2\"" >>"$rc"
fi
}
enable_service() {
ln -sfn "/etc/sv/$1" "$target/etc/runit/runsvdir/default/$1"
}
disable_service() {
rm -f "$target/etc/runit/runsvdir/default/$1"
}
configure() {
local home
# The live image's /etc/issue tells how to log in to the live system
printf 'Das OS Server \\r (\\l)\n\n' >"$target/etc/issue"
echo "$HOST_NAME" >"$target/etc/hostname"
if ! grep -q "[[:space:]]$HOST_NAME\$" "$target/etc/hosts"; then
printf '127.0.1.1\t%s\n' "$HOST_NAME" >>"$target/etc/hosts"
fi
echo "LANG=$LOCALE" >"$target/etc/locale.conf"
ln -sfn "/usr/share/zoneinfo/$TIMEZONE" "$target/etc/localtime"
set_rc KEYMAP "$KEYMAP"
set_rc HARDWARECLOCK UTC
[[ $LOCALE == uk* || $KEYMAP == ua* ]] && set_rc FONT cyr-sun16
# The administrator: sudo through wheel, root has no password
in_target useradd -m -s /bin/bash -G wheel,docker,socklog "$USER_NAME"
# -c: Void's PAM stack for chpasswd only has pam_permit for passwords, so
# without it nothing is written (Void's own installer does the same)
printf '%s:%s\n' "$USER_NAME" "$USER_PASSWORD" | in_target chpasswd -c SHA512
in_target passwd -l root >/dev/null
echo "%wheel ALL=(ALL:ALL) ALL" >"$target/etc/sudoers.d/10-wheel"
chmod 440 "$target/etc/sudoers.d/10-wheel"
if [[ -n $SSH_KEY ]]; then
home=$target/home/$USER_NAME
install -d -m 700 "$home/.ssh"
echo "$SSH_KEY" >"$home/.ssh/authorized_keys"
chmod 600 "$home/.ssh/authorized_keys"
in_target chown -R "$USER_NAME:$USER_NAME" "/home/$USER_NAME/.ssh"
fi
if [[ $SSH_PASSWORDS == no ]]; then
printf '%s\n' "# Written by das-server-install: SSH keys only" \
"PasswordAuthentication no" "KbdInteractiveAuthentication no" \
>"$target/etc/ssh/sshd_config.d/40-das-keys-only.conf"
fi
for sv in sshd chronyd nftables fail2ban docker socklog-unix nanoklogd; do
enable_service "$sv"
done
if [[ $NETWORK == NetworkManager ]]; then
disable_service dhcpcd
enable_service dbus
enable_service NetworkManager
else
enable_service dhcpcd
fi
serial_console && enable_service agetty-ttyS0
return 0
}
install_bootloader() {
local grub=$target/etc/default/grub cmdline="loglevel=4"
serial_console && cmdline+=" console=tty0 console=ttyS0,115200"
sed -i -e '/^GRUB_DISTRIBUTOR=/d' -e '/^GRUB_CMDLINE_LINUX_DEFAULT=/d' -e '/^GRUB_TIMEOUT=/d' \
-e '/^GRUB_TERMINAL/d' -e '/^GRUB_SERIAL_COMMAND=/d' -e '/^GRUB_DISABLE_OS_PROBER=/d' "$grub"
{
echo 'GRUB_DISTRIBUTOR="Das OS"'
echo "GRUB_CMDLINE_LINUX_DEFAULT=\"$cmdline\""
echo 'GRUB_TIMEOUT=5'
echo 'GRUB_DISABLE_OS_PROBER=true'
if serial_console; then
echo 'GRUB_TERMINAL="console serial"'
echo 'GRUB_SERIAL_COMMAND="serial --unit=0 --speed=115200"'
fi
} >>"$grub"
if uefi; then
in_target grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=dasos
# Also at the fallback path, for firmware that forgets boot entries
in_target grub-install --target=x86_64-efi --efi-directory=/boot/efi --removable --no-nvram
else
in_target grub-install --target=i386-pc "$DISK"
fi
}
finish() {
# The first snapshot, also shown in the boot menu (like Calamares' shellprocess@snapshot)
in_target snapper --no-dbus -c root create -d "Fresh install" -c number --userdata important=yes || true
in_target grub-mkconfig -o /boot/grub/grub.cfg
# The installer is not needed on the installed system
in_target xbps-remove -y das-server-installer das-install-common || true
sync
}
# Any failing command stops the installation with the log (set -E: also
# inside functions; "f || fail" would switch set -e off within f).
install_system() {
: >"$log"
trap fail ERR
step "$(t s_source)"
"$libexec/mount-source" >>"$log" 2>&1
# shellcheck disable=SC2059
step "$(printf "$(t s_disk)" "$DISK")"
partition >>"$log" 2>&1
step "$(t s_copy)"
copy_system >>"$log" 2>&1
step "$(t s_config)"
write_fstab >>"$log" 2>&1
mount_chroot >>"$log" 2>&1
configure >>"$log" 2>&1
step "$(t s_post)"
in_target "$libexec/postinstall" >>"$log" 2>&1
step "$(t s_boot)"
install_bootloader >>"$log" 2>&1
finish >>"$log" 2>&1
unmount_target >>"$log" 2>&1
trap - ERR
}
load_config() {
local file=$1
[[ -r $file ]] || {
echo "das-server-install: cannot read $file" >&2
exit 1
}
# shellcheck source=/dev/null
. "$file"
interactive=""
local bad=""
[[ -b $DISK ]] || bad+=" DISK"
valid_host "$HOST_NAME" || bad+=" HOST_NAME"
valid_user "$USER_NAME" || bad+=" USER_NAME"
[[ -n $USER_PASSWORD ]] || bad+=" USER_PASSWORD"
valid_key "$SSH_KEY" || bad+=" SSH_KEY"
[[ $SSH_PASSWORDS == yes || ( $SSH_PASSWORDS == no && -n $SSH_KEY ) ]] || bad+=" SSH_PASSWORDS"
[[ -f /usr/share/zoneinfo/$TIMEZONE ]] || bad+=" TIMEZONE"
[[ $NETWORK == dhcpcd || $NETWORK == NetworkManager ]] || bad+=" NETWORK"
[[ $ERASE_DISK == yes ]] || bad+=" ERASE_DISK"
if [[ -n $bad ]]; then
echo "das-server-install: invalid or missing in $file:$bad" >&2
exit 1
fi
}
main() {
if [[ $EUID -ne 0 ]]; then
exec sudo --preserve-env=LANG "$0" "$@"
fi
case ${1:-} in
--config)
load_config "${2:?--config needs a file}"
;;
-h | --help)
sed -n '2,11s/^# \{0,1\}//p' "$0"
exit 0
;;
"") ;;
*)
echo "das-server-install: unknown option $1" >&2
exit 1
;;
esac
if [[ -n $interactive ]]; then
ask_language
ask_disk
ask_host
ask_user
ask_ssh
ask_system
confirm
fi
install_system
if [[ -n $interactive ]]; then
local msg
# shellcheck disable=SC2059
msg=$(printf "$(t finished)" "$USER_NAME")
if dialog_ui --yesno "$msg\n\n$(t reboot)" 12 70; then
reboot
fi
clear
else
echo "==> done, log: $log"
fi
}
main "$@"
@@ -0,0 +1,20 @@
# Answers for an unattended Das OS Server installation:
# sudo das-server-install --config install.conf
# The file is read by bash. Every value is checked before anything is done.
# The disk to install to; it is erased completely
DISK=/dev/vda
ERASE_DISK=no # must be "yes" to install
HOST_NAME=dasos-server
USER_NAME=admin # gets sudo; root login stays disabled
USER_PASSWORD=change-me
# A public key for SSH logins (optional), and whether passwords stay allowed
SSH_KEY=""
SSH_PASSWORDS=yes # "no" requires SSH_KEY
TIMEZONE=Europe/Kyiv
KEYMAP=us # console layout: us, ua-utf, uk, de-latin1, ...
LOCALE=en_US.UTF-8 # or uk_UA.UTF-8
NETWORK=dhcpcd # or NetworkManager
+16
View File
@@ -0,0 +1,16 @@
# Template file for 'das-server-installer'
pkgname=das-server-installer
version=0.2.0
revision=1
depends="bash dialog das-install-common btrfs-progs dosfstools gptfdisk parted
rsync util-linux grub grub-x86_64-efi efibootmgr"
short_desc="Das OS Server console installer for the live ISO"
maintainer="Das <anry.das@gmail.com>"
license="MIT"
homepage="https://git.das.org.ua/das/Das-OS"
do_install() {
vbin ${FILESDIR}/das-server-install
vsconf ${FILESDIR}/install.conf
vlicense ${FILESDIR}/LICENSE
}
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Das
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,3 @@
{
"log-driver": "local"
}
@@ -0,0 +1,15 @@
# Das OS: ban addresses that keep failing SSH logins, through nftables.
[DEFAULT]
banaction = nftables-multiport
banaction_allports = nftables-allports
[sshd]
enabled = true
backend = polling
# socklog-void collects auth messages here. svlogd starts each line with a
# UTC time stamp; the pattern also consumes the facility and the syslog date
# that follow it, so the sshd filter sees "sshd-session[pid]: ...":
# 2026-10-19T17:25:46.09500 auth.info: Oct 19 20:25:46 sshd-session[1154]: Invalid user ...
logpath = /var/log/socklog/secure/current
datepattern = ^%%Y-%%m-%%dT%%H:%%M:%%S\.%%f(?: \S+: \S+ +\d+ \S+)?
logtimezone = UTC
+47
View File
@@ -0,0 +1,47 @@
#!/usr/sbin/nft -f
# Das OS server firewall: incoming connections are closed except SSH.
#
# Open ports with dfw (dfw allow 80/tcp), which keeps them in
# /etc/nftables.d/dfw.nft. Ports that Docker containers publish (-p 8080:80)
# pass through Docker's own forwarding rules, not the input chain below.
#
# After editing this file: dfw reload. Only the "das" table is replaced, so the
# rules of Docker and fail2ban stay. Do not restart the nftables service for
# that: its finish script flushes the whole ruleset, theirs too.
table inet das
delete table inet das
table inet das {
# Filled by dfw
set tcp_ports {
type inet_service; flags interval; auto-merge;
}
set udp_ports {
type inet_service; flags interval; auto-merge;
}
chain input {
type filter hook input priority filter; policy drop;
ct state established,related accept
ct state invalid drop
iif lo accept
meta l4proto { icmp, ipv6-icmp } accept
# DHCPv6 answers come from the router's link-local address
ip6 saddr fe80::/10 udp dport dhcpv6-client accept
# Containers may reach services on the host (DNS, a database...)
iifname "docker0" accept
iifname "br-*" accept
tcp dport ssh accept
tcp dport @tcp_ports accept
udp dport @udp_ports accept
# Rules of your own go here, e.g. a port for one network only:
# ip saddr 192.168.1.0/24 tcp dport 5432 accept
}
}
# dfw's ports and drop-ins of your own
include "/etc/nftables.d/*.nft"
+4
View File
@@ -0,0 +1,4 @@
# Das OS server defaults, see sshd_config(5). sshd keeps the first value it
# reads, so a file sorted before this one (40-local.conf) overrides it.
PermitRootLogin prohibit-password
X11Forwarding no
@@ -0,0 +1,5 @@
# Firmware a server does not need, otherwise pulled in by linux-base:
# NVIDIA GPUs and sound cards. Remove a line to get the package back.
ignorepkg=linux-firmware-nvidia
ignorepkg=sof-firmware
ignorepkg=alsa-firmware
+27
View File
@@ -0,0 +1,27 @@
# Template file for 'das-server'
pkgname=das-server
version=0.2.0
revision=1
depends="openssh chrony dhcpcd NetworkManager docker docker-compose nftables
fail2ban socklog-void python3 curl lazydocker lazygit mc tmux htop
das-snapshots das-update das-helpers xtools-minimal"
short_desc="Das OS server: packages and secure defaults for SSH, firewall and Docker"
maintainer="Das <anry.das@gmail.com>"
license="MIT"
homepage="https://git.das.org.ua/das/Das-OS"
conf_files="
/etc/nftables.conf
/etc/fail2ban/jail.d/das.conf
/etc/ssh/sshd_config.d/50-das.conf
/etc/docker/daemon.json
/etc/xbps.d/das-server.conf"
do_install() {
vinstall ${FILESDIR}/nftables.conf 644 etc
vmkdir etc/nftables.d
vinstall ${FILESDIR}/fail2ban-das.conf 644 etc/fail2ban/jail.d das.conf
vinstall ${FILESDIR}/sshd-das.conf 644 etc/ssh/sshd_config.d 50-das.conf
vinstall ${FILESDIR}/docker-daemon.json 644 etc/docker daemon.json
vinstall ${FILESDIR}/xbps-ignore.conf 644 etc/xbps.d das-server.conf
vlicense ${FILESDIR}/LICENSE
}
@@ -4,6 +4,21 @@
# which is only known inside the session. Runs once per user. # which is only known inside the session. Runs once per user.
set -u set -u
# Sets an xfconf property, creating it when it does not exist yet.
set_prop() {
local channel=$1 prop=$2 type=$3 value=$4
xfconf-query -c "$channel" -p "$prop" -s "$value" 2>/dev/null ||
xfconf-query -c "$channel" -p "$prop" -n -t "$type" -s "$value"
}
# The live session must not lock the screen: the installer runs for minutes
# without input, and the live user's password is not something people know.
# Static files would reach the installed system, which copies the live image.
if grep -q '\(^\| \)root=live:' /proc/cmdline; then
set_prop xfce4-screensaver /lock/enabled bool false
set_prop xfce4-power-manager /xfce4-power-manager/lock-screen-suspend-hibernate bool false
fi
marker="${XDG_CONFIG_HOME:-$HOME/.config}/das/session-init.done" marker="${XDG_CONFIG_HOME:-$HOME/.config}/das/session-init.done"
[[ -e $marker ]] && exit 0 [[ -e $marker ]] && exit 0
@@ -12,13 +27,6 @@ if [[ $(xfconf-query -c xsettings -p /Net/ThemeName 2>/dev/null) == *-Dark ]]; t
wallpaper=/usr/share/backgrounds/das/das-dark.svg wallpaper=/usr/share/backgrounds/das/das-dark.svg
fi fi
# Sets an xfconf property, creating it when it does not exist yet.
set_prop() {
local prop=$1 type=$2 value=$3
xfconf-query -c xfce4-desktop -p "$prop" -s "$value" 2>/dev/null ||
xfconf-query -c xfce4-desktop -p "$prop" -n -t "$type" -s "$value"
}
# "xrandr --listactivemonitors" prints a header, then one line per monitor ending in its connector name. # "xrandr --listactivemonitors" prints a header, then one line per monitor ending in its connector name.
monitors=$(xrandr --listactivemonitors 2>/dev/null | awk 'NR > 1 { print $NF }') monitors=$(xrandr --listactivemonitors 2>/dev/null | awk 'NR > 1 { print $NF }')
if [[ -z $monitors ]]; then if [[ -z $monitors ]]; then
@@ -28,8 +36,8 @@ fi
for monitor in $monitors; do for monitor in $monitors; do
base="/backdrop/screen0/monitor${monitor}/workspace0" base="/backdrop/screen0/monitor${monitor}/workspace0"
set_prop "$base/last-image" string "$wallpaper" set_prop xfce4-desktop "$base/last-image" string "$wallpaper"
set_prop "$base/image-style" int 5 # zoomed: fill the screen, keep proportions set_prop xfce4-desktop "$base/image-style" int 5 # zoomed: fill the screen, keep proportions
done done
mkdir -p "${marker%/*}" && touch "$marker" mkdir -p "${marker%/*}" && touch "$marker"
@@ -0,0 +1,8 @@
# Sourced by /etc/sv/lightdm/run (installed by das-settings).
# runit starts services with an empty environment, so pass the system language
# from /etc/locale.conf to LightDM: the greeter and new sessions use it.
if [ -r /etc/locale.conf ]; then
set -a
. /etc/locale.conf
set +a
fi
@@ -0,0 +1,10 @@
# Sourced by LightDM's session wrapper before the Xfce session starts.
#
# xdg-user-dirs 0.20 loads its translations only from the first XDG_DATA_DIRS
# entry with a locale directory, the empty /usr/local/share/locale, and so
# creates Desktop, Documents... in English whatever the language. Create the
# folders here with a lookup path that finds the translations; the later run
# from the Xfce session then keeps them.
if command -v xdg-user-dirs-update >/dev/null 2>&1; then
XDG_DATA_DIRS=/usr/share xdg-user-dirs-update
fi
+4 -2
View File
@@ -4,7 +4,7 @@
# Defaults go to /etc/xdg/das, which /etc/profile.d/das-xdg.sh puts in front of # Defaults go to /etc/xdg/das, which /etc/profile.d/das-xdg.sh puts in front of
# /etc/xdg: stock Xfce files stay untouched and users' own changes still win. # /etc/xdg: stock Xfce files stay untouched and users' own changes still win.
pkgname=das-settings pkgname=das-settings
version=0.1.0 version=0.4.0
revision=1 revision=1
create_wrksrc=yes create_wrksrc=yes
depends="das-theme lightdm lightdm-gtk-greeter xfce4-notifyd xfce4-panel depends="das-theme lightdm lightdm-gtk-greeter xfce4-notifyd xfce4-panel
@@ -14,7 +14,7 @@ short_desc="Das OS desktop defaults: Xfce panel, theme, fonts, login screen"
maintainer="Das <anry.das@gmail.com>" maintainer="Das <anry.das@gmail.com>"
license="MIT" license="MIT"
homepage="https://git.das.org.ua/das/Das-OS" homepage="https://git.das.org.ua/das/Das-OS"
conf_files="/etc/profile.d/das-xdg.sh /etc/xdg/das/xfce4/xfconf/xfce-perchannel-xml/*.xml" conf_files="/etc/profile.d/das-xdg.sh /etc/sv/lightdm/conf /etc/xdg/das/xfce4/xfconf/xfce-perchannel-xml/*.xml"
do_install() { do_install() {
local xml local xml
@@ -26,8 +26,10 @@ do_install() {
vinstall "${FILESDIR}/bin/das-session-init" 755 usr/libexec/das vinstall "${FILESDIR}/bin/das-session-init" 755 usr/libexec/das
vinstall "${FILESDIR}/autostart/das-session-init.desktop" 644 etc/xdg/autostart vinstall "${FILESDIR}/autostart/das-session-init.desktop" 644 etc/xdg/autostart
vinstall "${FILESDIR}/xinitrc.d/50-das-user-dirs" 755 etc/X11/xinit/xinitrc.d
vinstall "${FILESDIR}/lightdm/50-das.conf" 644 usr/share/lightdm/lightdm.conf.d vinstall "${FILESDIR}/lightdm/50-das.conf" 644 usr/share/lightdm/lightdm.conf.d
vinstall "${FILESDIR}/lightdm/sv-conf" 644 etc/sv/lightdm conf
vinstall "${FILESDIR}/lightdm/50-das-greeter.conf" 644 \ vinstall "${FILESDIR}/lightdm/50-das-greeter.conf" 644 \
usr/share/lightdm/lightdm-gtk-greeter.conf.d 50-das.conf usr/share/lightdm/lightdm-gtk-greeter.conf.d 50-das.conf
vlicense "${FILESDIR}/LICENSE" vlicense "${FILESDIR}/LICENSE"
+1
View File
@@ -0,0 +1 @@
../../../LICENSE
+51
View File
@@ -0,0 +1,51 @@
#!/bin/sh
# Set up snapper for a Das OS root file system on Btrfs.
#
# Expects the layout created by the installer: the root in subvolume @ and
# snapshots in subvolume @snapshots mounted at /.snapshots, which is what
# snapper-rollback needs. Safe to run again.
set -eu
msg() { echo "das-snapper-setup: $*"; }
if [ "$(id -u)" -ne 0 ]; then
msg "must be run as root" >&2
exit 1
fi
if [ "$(findmnt -no FSTYPE /)" != btrfs ]; then
msg "the root file system is not Btrfs, nothing to do"
exit 0
fi
if [ "$(findmnt -no FSTYPE /.snapshots 2>/dev/null)" != btrfs ]; then
msg "/.snapshots is not a mounted Btrfs subvolume" >&2
exit 1
fi
if [ ! -e /etc/snapper/configs/root ]; then
install -m 640 /usr/share/das/snapper/root /etc/snapper/configs/root
msg "created /etc/snapper/configs/root"
fi
if ! grep -q '^SNAPPER_CONFIGS=".*\broot\b' /etc/conf.d/snapper; then
sed -i 's/^SNAPPER_CONFIGS=.*/SNAPPER_CONFIGS="root"/' /etc/conf.d/snapper
fi
chmod 750 /.snapshots
chgrp wheel /.snapshots
# snapper-rollback mounts the top-level subvolume from this device.
dev=$(findmnt -no SOURCE / | sed 's/\[.*\]$//')
uuid=$(blkid -s UUID -o value "$dev")
sed -i "s|^#\?\s*dev = .*|dev = /dev/disk/by-uuid/$uuid|" /etc/snapper-rollback.conf
# Name the GRUB submenu that grub-btrfs adds
cfg=/etc/default/grub-btrfs/config
if [ -f "$cfg" ] && ! grep -q '^GRUB_BTRFS_SUBMENUNAME=' "$cfg"; then
echo 'GRUB_BTRFS_SUBMENUNAME="Das OS snapshots"' >>"$cfg"
fi
# Hourly snapshots come from cron, and grub-btrfs adds them to the boot menu.
# snapper talks to snapperd over the system bus (the server has no desktop
# that would enable dbus).
for sv in dbus cronie grub-btrfs; do
ln -sfn "/etc/sv/$sv" "/etc/runit/runsvdir/default/$sv"
done
msg "snapshots are enabled"
@@ -0,0 +1,2 @@
# Lets a read-only snapper snapshot boot from the GRUB menu (see das-snapshot-overlay).
add_dracutmodules+=" das-snapshot-overlay "
@@ -0,0 +1,14 @@
#!/bin/sh
# Snapper snapshots are read-only, and the system can't start from a read-only
# root. When GRUB boots a snapshot, mount it under an overlay whose writable
# layer lives in RAM. Changes made in that session are lost at shutdown.
[ "$(btrfs property get -ts "$NEWROOT" ro 2>/dev/null)" = "ro=true" ] || return 0
info "das-snapshot-overlay: read-only snapshot, adding a writable overlay"
lower=$(mktemp -d -p /)
ram=$(mktemp -d -p /)
mount --move "$NEWROOT" "$lower"
mount -t tmpfs -o mode=0755 das-overlay "$ram"
mkdir -p "$ram/upper" "$ram/work"
mount -t overlay -o "lowerdir=$lower,upperdir=$ram/upper,workdir=$ram/work" das-overlay "$NEWROOT"
@@ -0,0 +1,20 @@
#!/bin/bash
# Dracut module: boot read-only Btrfs snapshots with a writable overlay in RAM.
check() {
require_binaries btrfs || return 1
return 0
}
depends() {
echo btrfs
}
installkernel() {
hostonly='' instmods overlay
}
install() {
inst_multiple btrfs mktemp
inst_hook pre-pivot 10 "$moddir/das-snapshot-overlay.sh"
}
@@ -0,0 +1,37 @@
# Snapper configuration for the Das OS root file system.
# das-snapper-setup copies it to /etc/snapper/configs/root.
SUBVOLUME="/"
FSTYPE="btrfs"
QGROUP=""
# Snapshots may use at most half of the file system and must leave 20% free.
SPACE_LIMIT="0.5"
FREE_LIMIT="0.2"
# Members of wheel can list and compare snapshots without sudo.
ALLOW_USERS=""
ALLOW_GROUPS="wheel"
SYNC_ACL="yes"
BACKGROUND_COMPARISON="yes"
# Snapshots made by das-update and by hand.
NUMBER_CLEANUP="yes"
NUMBER_MIN_AGE="3600"
NUMBER_LIMIT="20"
NUMBER_LIMIT_IMPORTANT="10"
# Hourly snapshots made by cron.
TIMELINE_CREATE="yes"
TIMELINE_CLEANUP="yes"
TIMELINE_MIN_AGE="3600"
TIMELINE_LIMIT_HOURLY="5"
TIMELINE_LIMIT_DAILY="7"
TIMELINE_LIMIT_WEEKLY="2"
TIMELINE_LIMIT_MONTHLY="0"
TIMELINE_LIMIT_QUARTERLY="0"
TIMELINE_LIMIT_YEARLY="0"
EMPTY_PRE_POST_CLEANUP="yes"
EMPTY_PRE_POST_MIN_AGE="3600"
+20
View File
@@ -0,0 +1,20 @@
# Template file for 'das-snapshots'
pkgname=das-snapshots
version=0.2.0
revision=1
depends="btrfs-progs cronie dracut grub-btrfs snapper snapper-rollback"
short_desc="Das OS Btrfs snapshots: snapper setup, boot menu entries and rollback"
maintainer="Das <anry.das@gmail.com>"
license="MIT"
homepage="https://git.das.org.ua/das/Das-OS"
do_install() {
vbin ${FILESDIR}/das-snapper-setup
vinstall ${FILESDIR}/snapper-root.conf 644 usr/share/das/snapper root
vinstall ${FILESDIR}/das-snapshots.conf 644 usr/lib/dracut/dracut.conf.d 50-das-snapshots.conf
vinstall ${FILESDIR}/dracut/90das-snapshot-overlay/module-setup.sh 755 \
usr/lib/dracut/modules.d/90das-snapshot-overlay
vinstall ${FILESDIR}/dracut/90das-snapshot-overlay/das-snapshot-overlay.sh 755 \
usr/lib/dracut/modules.d/90das-snapshot-overlay
vlicense ${FILESDIR}/LICENSE
}
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Das
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,7 @@
[Desktop Entry]
Type=Application
Name=Das OS update check
Comment=Shows a tray icon when updates are available
Exec=das-update --tray
OnlyShowIn=XFCE;
NoDisplay=true
@@ -0,0 +1,12 @@
[Desktop Entry]
Type=Application
Name=System Update
Name[uk]=Оновлення системи
Comment=Install updates with a snapshot to roll back to
Comment[uk]=Встановити оновлення зі знімком для відкату
Exec=das-update
Icon=software-update-available
Terminal=false
Categories=System;Settings;
Keywords=update;upgrade;xbps;snapshot;
Keywords[uk]=оновлення;xbps;знімок;
@@ -0,0 +1,6 @@
[Desktop Entry]
Type=Application
Name=Das OS welcome
Exec=das-welcome --autostart
OnlyShowIn=XFCE;
NoDisplay=true
@@ -0,0 +1,10 @@
[Desktop Entry]
Type=Application
Name=Welcome to Das OS
Name[uk]=Ласкаво просимо до Das OS
Comment=Language, home folders, updates and help
Comment[uk]=Мова, теки, оновлення та довідка
Exec=das-welcome
Icon=das-logo
Terminal=false
Categories=System;
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
"http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
<policyconfig>
<vendor>Das OS</vendor>
<vendor_url>https://git.das.org.ua/das/Das-OS</vendor_url>
<action id="ua.org.das.update">
<description>Update the system</description>
<description xml:lang="uk">Оновлення системи</description>
<message>Authentication is required to install system updates</message>
<message xml:lang="uk">Для встановлення оновлень потрібна автентифікація</message>
<icon_name>software-update-available</icon_name>
<defaults>
<allow_any>auth_admin</allow_any>
<allow_inactive>auth_admin</allow_inactive>
<allow_active>auth_admin_keep</allow_active>
</defaults>
<annotate key="org.freedesktop.policykit.exec.path">/usr/bin/das-update</annotate>
</action>
</policyconfig>
@@ -0,0 +1,27 @@
// Command das-update-gui is the desktop part of das-update, installed as
// /usr/libexec/das/das-update-gui by das-tools. das-update starts it when a
// display is available, so servers get das-update without GTK.
//
// das-update-gui window that lists and installs updates
// das-update-gui --tray background check, shows a tray icon when updates are available
package main
import (
"flag"
"os"
"dasos/tools/internal/i18n"
)
func main() {
tray := flag.Bool("tray", false, "check for updates in the background")
lang := flag.String("lang", "", "message language, e.g. uk_UA.UTF-8")
flag.Parse()
if *lang != "" {
i18n.SetLang(*lang)
}
if *tray {
os.Exit(runTray())
}
os.Exit(runWindow())
}
@@ -0,0 +1,147 @@
package main
import (
"context"
"fmt"
"os"
"os/exec"
"slices"
"time"
"github.com/gotk3/gotk3/glib"
"github.com/gotk3/gotk3/gtk"
"dasos/tools/internal/i18n"
"dasos/tools/internal/system"
"dasos/tools/internal/tray"
"dasos/tools/internal/xbps"
)
const (
// Give the network time to come up after login.
firstCheck = 2 * time.Minute
checkInterval = 6 * time.Hour
checkTimeout = 5 * time.Minute
// How often to look for updates installed outside the tray: from a
// separately started window, das-update --cli or xbps-install.
pkgdbInterval = time.Minute
)
type trayApp struct {
icon *tray.Icon
checking bool
notified []string // the updates the last notification was about
window *exec.Cmd
pkgdb time.Time // package database time at the last check
}
func runTray() int {
if system.IsLive() {
// Updating the live session only fills its RAM overlay.
return 0
}
gtk.Init(nil)
t := &trayApp{}
menu, err := gtk.MenuNew()
if err != nil {
fmt.Fprintln(os.Stderr, "das-update:", err)
return 1
}
for _, item := range []struct {
label string
action func()
}{
{i18n.T("update.tray.menu"), t.openWindow},
{i18n.T("update.tray.check"), t.check},
} {
mi, err := gtk.MenuItemNewWithLabel(item.label)
if err != nil {
fmt.Fprintln(os.Stderr, "das-update:", err)
return 1
}
mi.Connect("activate", item.action)
menu.Append(mi)
}
t.icon = tray.New(iconName, t.openWindow, menu)
glib.TimeoutSecondsAdd(uint(firstCheck.Seconds()), func() bool {
t.check()
glib.TimeoutSecondsAdd(uint(checkInterval.Seconds()), func() bool {
t.check()
return true
})
return false
})
glib.TimeoutSecondsAdd(uint(pkgdbInterval.Seconds()), func() bool {
if t.notified != nil && !xbps.PkgdbTime().Equal(t.pkgdb) {
t.check()
}
return true
})
gtk.Main()
return 0
}
func (t *trayApp) check() {
if t.checking {
return
}
t.checking = true
t.pkgdb = xbps.PkgdbTime()
go func() {
ctx, cancel := context.WithTimeout(context.Background(), checkTimeout)
defer cancel()
updates, err := xbps.Check(ctx)
glib.IdleAdd(func() { t.show(updates, err) })
}()
}
func (t *trayApp) show(updates []xbps.Update, err error) {
t.checking = false
if err != nil {
// Offline or a mirror problem: keep the current state, try again later.
fmt.Fprintln(os.Stderr, "das-update:", err)
return
}
if len(updates) == 0 {
t.icon.Hide()
t.notified = nil
return
}
summary := i18n.Plural("update.count", len(updates))
t.icon.Show(summary)
names := make([]string, len(updates))
for i, u := range updates {
names[i] = u.Name + "-" + u.NewVersion
}
// Notify once per set of updates, not at every check.
if !slices.Equal(names, t.notified) {
t.notified = names
exec.Command("notify-send", "--app-name=das-update", "--icon="+iconName,
summary, i18n.T("update.notify")).Start()
}
}
// openWindow starts the update window and checks again once it is closed.
func (t *trayApp) openWindow() {
if t.window != nil {
// das-update is a single-instance application: this raises it.
exec.Command("/usr/bin/das-update").Start()
return
}
cmd := exec.Command("/usr/bin/das-update")
if err := cmd.Start(); err != nil {
fmt.Fprintln(os.Stderr, "das-update:", err)
return
}
t.window = cmd
go func() {
cmd.Wait()
glib.IdleAdd(func() {
t.window = nil
t.check()
})
}()
}
@@ -0,0 +1,340 @@
package main
import (
"bufio"
"context"
"errors"
"fmt"
"html"
"os"
"os/exec"
"strconv"
"strings"
"github.com/gotk3/gotk3/glib"
"github.com/gotk3/gotk3/gtk"
"dasos/tools/internal/i18n"
"dasos/tools/internal/system"
"dasos/tools/internal/update"
"dasos/tools/internal/xbps"
)
const (
appID = "ua.org.das.Update"
iconName = "software-update-available"
// pkexec exit codes when the password dialog was dismissed or failed
pkexecDismissed = 126
pkexecNotAuthed = 127
)
type window struct {
win *gtk.ApplicationWindow
status *gtk.Label
spinner *gtk.Spinner
store *gtk.ListStore
list *gtk.ScrolledWindow
info *gtk.Label
details *gtk.Expander
log *gtk.TextView
check *gtk.Button
install *gtk.Button
close *gtk.Button
running bool
snapshot int
}
func runWindow() int {
app, err := gtk.ApplicationNew(appID, glib.APPLICATION_FLAGS_NONE)
if err != nil {
fmt.Fprintln(os.Stderr, "das-update:", err)
return 1
}
var w *window
app.Connect("activate", func() {
// A second "das-update" only raises the open window.
if w != nil {
w.win.Present()
return
}
w, err = newWindow(app)
if err != nil {
fmt.Fprintln(os.Stderr, "das-update:", err)
app.Quit()
return
}
w.win.ShowAll()
w.details.Hide()
w.startCheck()
})
return app.Run(nil)
}
func newWindow(app *gtk.Application) (*window, error) {
w := &window{}
var err error
must := func(e error) {
if err == nil {
err = e
}
}
w.win, err = gtk.ApplicationWindowNew(app)
if err != nil {
return nil, err
}
w.win.SetTitle(i18n.T("update.title"))
w.win.SetIconName(iconName)
w.win.SetDefaultSize(620, 480)
w.win.SetPosition(gtk.WIN_POS_CENTER)
w.win.Connect("delete-event", func() bool { return w.running })
box, e := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 12)
must(e)
box.SetMarginStart(18)
box.SetMarginEnd(18)
box.SetMarginTop(18)
box.SetMarginBottom(18)
header, e := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 12)
must(e)
icon, e := gtk.ImageNewFromIconName(iconName, gtk.ICON_SIZE_DIALOG)
must(e)
w.status, e = gtk.LabelNew("")
must(e)
w.status.SetXAlign(0)
w.status.SetLineWrap(true)
w.spinner, e = gtk.SpinnerNew()
must(e)
header.PackStart(icon, false, false, 0)
header.PackStart(w.status, true, true, 0)
header.PackEnd(w.spinner, false, false, 0)
box.PackStart(header, false, false, 0)
w.store, e = gtk.ListStoreNew(glib.TYPE_STRING, glib.TYPE_STRING, glib.TYPE_STRING)
must(e)
tree, e := gtk.TreeViewNewWithModel(w.store)
must(e)
for i, title := range []string{"Пакет|Package", "Встановлено|Installed", "Нова версія|New version"} {
r, e := gtk.CellRendererTextNew()
must(e)
col, e := gtk.TreeViewColumnNewWithAttribute(pick(title), r, "text", i)
must(e)
col.SetResizable(true)
tree.AppendColumn(col)
}
w.list, e = gtk.ScrolledWindowNew(nil, nil)
must(e)
w.list.SetShadowType(gtk.SHADOW_IN)
w.list.SetVExpand(true)
w.list.Add(tree)
box.PackStart(w.list, true, true, 0)
w.info, e = gtk.LabelNew("")
must(e)
w.info.SetXAlign(0)
w.info.SetLineWrap(true)
w.info.SetSelectable(true)
box.PackStart(w.info, false, false, 0)
w.details, e = gtk.ExpanderNew(i18n.T("update.details"))
must(e)
w.log, e = gtk.TextViewNew()
must(e)
w.log.SetEditable(false)
w.log.SetCursorVisible(false)
w.log.SetMonospace(true)
w.log.SetWrapMode(gtk.WRAP_WORD_CHAR)
logScroll, e := gtk.ScrolledWindowNew(nil, nil)
must(e)
logScroll.SetShadowType(gtk.SHADOW_IN)
logScroll.SetSizeRequest(-1, 180)
logScroll.SetVExpand(true)
logScroll.Add(w.log)
w.details.Add(logScroll)
box.PackStart(w.details, true, true, 0)
buttons, e := gtk.ButtonBoxNew(gtk.ORIENTATION_HORIZONTAL)
must(e)
buttons.SetLayout(gtk.BUTTONBOX_END)
buttons.SetSpacing(6)
w.check, e = gtk.ButtonNewWithLabel(i18n.T("update.check"))
must(e)
w.install, e = gtk.ButtonNewWithLabel(i18n.T("update.install"))
must(e)
w.close, e = gtk.ButtonNewWithLabel(i18n.T("update.close"))
must(e)
if e == nil {
style, _ := w.install.GetStyleContext()
style.AddClass("suggested-action")
}
buttons.Add(w.check)
buttons.Add(w.install)
buttons.Add(w.close)
box.PackEnd(buttons, false, false, 0)
w.check.Connect("clicked", w.startCheck)
w.install.Connect("clicked", w.startInstall)
w.close.Connect("clicked", func() { w.win.Close() })
w.win.Add(box)
return w, err
}
// pick chooses from an inline "Ukrainian|English" pair.
func pick(pair string) string {
uk, en, _ := strings.Cut(pair, "|")
if strings.HasPrefix(system.Lang(), "uk") {
return uk
}
return en
}
func (w *window) setBusy(busy bool, status string) {
w.running = busy
w.status.SetMarkup("<b><big>" + html.EscapeString(status) + "</big></b>")
if busy {
w.spinner.Start()
} else {
w.spinner.Stop()
}
w.check.SetSensitive(!busy)
w.install.SetSensitive(!busy)
w.close.SetSensitive(!busy)
}
func (w *window) startCheck() {
w.setBusy(true, i18n.T("update.checking"))
w.info.SetText("")
go func() {
updates, err := xbps.Check(context.Background())
glib.IdleAdd(func() { w.showUpdates(updates, err) })
}()
}
func (w *window) showUpdates(updates []xbps.Update, err error) {
w.store.Clear()
switch {
case err != nil:
w.setBusy(false, i18n.T("update.checkfailed", err))
w.install.SetSensitive(false)
return
case len(updates) == 0:
w.setBusy(false, i18n.T("update.uptodate"))
w.install.SetSensitive(false)
w.list.Hide()
return
}
for _, u := range updates {
newVersion := u.NewVersion
if u.Action == "remove" {
newVersion = "−"
}
w.store.Set(w.store.Append(), []int{0, 1, 2}, []any{u.Name, u.OldVersion, newVersion})
}
w.details.Hide()
w.list.Show()
w.setBusy(false, i18n.Plural("update.count", len(updates)))
w.install.GrabFocus()
}
func (w *window) startInstall() {
w.setBusy(true, i18n.T("update.running"))
w.snapshot = 0
w.info.SetText("")
buf, _ := w.log.GetBuffer()
buf.SetText("")
// The log takes the place of the package list while xbps works.
w.list.Hide()
w.details.Show()
w.details.SetExpanded(true)
cmd := exec.Command("pkexec", "/usr/bin/das-update", "--apply", "--lang", system.Lang())
out, err := cmd.StdoutPipe()
if err == nil {
cmd.Stderr = cmd.Stdout
err = cmd.Start()
}
if err != nil {
w.finish(false, false, err.Error())
return
}
go func() {
var reboot, upToDate bool
var failure string
sc := bufio.NewScanner(out)
for sc.Scan() {
line := sc.Text()
switch {
case strings.HasPrefix(line, update.MarkStep):
line = "\n==> " + strings.TrimPrefix(line, update.MarkStep)
case strings.HasPrefix(line, update.MarkSnapshot):
n, _ := strconv.Atoi(strings.TrimPrefix(line, update.MarkSnapshot))
glib.IdleAdd(func() { w.snapshot = n })
line = i18n.T("update.snapshot", n)
case line == update.MarkReboot:
reboot = true
continue
case line == update.MarkUpToDate:
upToDate = true
continue
case strings.HasPrefix(line, update.MarkError):
failure = strings.TrimPrefix(line, update.MarkError)
}
glib.IdleAdd(func() { w.appendLog(line + "\n") })
}
err := cmd.Wait()
var exit *exec.ExitError
if errors.As(err, &exit) && (exit.ExitCode() == pkexecDismissed || exit.ExitCode() == pkexecNotAuthed) && failure == "" {
glib.IdleAdd(func() { w.cancelled() })
return
}
if err != nil && failure == "" {
failure = err.Error()
}
glib.IdleAdd(func() {
if upToDate {
w.setBusy(false, i18n.T("update.uptodate"))
w.store.Clear()
w.install.SetSensitive(false)
return
}
w.finish(failure == "", reboot, failure)
})
}()
}
func (w *window) appendLog(text string) {
buf, _ := w.log.GetBuffer()
buf.Insert(buf.GetEndIter(), text)
buf.PlaceCursor(buf.GetEndIter())
w.log.ScrollToMark(buf.GetInsert(), 0, false, 0, 0)
}
func (w *window) cancelled() {
w.setBusy(false, i18n.T("update.cancelled"))
w.details.Hide()
w.list.Show()
}
func (w *window) finish(ok, reboot bool, failure string) {
var info []string
if ok {
w.setBusy(false, i18n.T("update.done"))
w.store.Clear()
w.install.SetSensitive(false)
if reboot {
info = append(info, i18n.T("update.reboot"))
}
} else {
w.setBusy(false, i18n.T("update.failed"))
w.details.SetExpanded(true)
if failure != "" {
w.appendLog("\n" + failure + "\n")
}
}
if w.snapshot > 0 {
info = append(info, i18n.T("update.snapshot", w.snapshot), i18n.T("update.rollback", w.snapshot, w.snapshot))
}
w.info.SetText(strings.Join(info, "\n"))
}
@@ -0,0 +1,202 @@
// Command das-update updates Das OS with a snapper snapshot taken before
// the update, so a broken update can be rolled back from the boot menu.
//
// das-update the update window on a desktop, otherwise the same as --cli
// das-update --tray background check, shows a tray icon when updates are available
// das-update --cli update in a terminal (servers, ssh); -y skips the question
// das-update --apply the privileged part, started by the window through pkexec
//
// The window and the tray icon are a separate program from das-tools
// (guiPath), so this one has no GTK dependency.
package main
import (
"bufio"
"context"
"flag"
"fmt"
"io"
"os"
"os/exec"
"os/signal"
"strings"
"syscall"
"dasos/tools/internal/i18n"
"dasos/tools/internal/system"
"dasos/tools/internal/update"
"dasos/tools/internal/xbps"
)
func main() {
cli := flag.Bool("cli", false, "update in the terminal")
yes := flag.Bool("y", false, "with --cli: do not ask for confirmation")
apply := flag.Bool("apply", false, "install the updates as root (used by the window)")
tray := flag.Bool("tray", false, "check for updates in the background")
lang := flag.String("lang", "", "message language, e.g. uk_UA.UTF-8 (pkexec clears LANG)")
flag.Parse()
if *lang != "" {
i18n.SetLang(*lang)
}
switch {
case *apply:
os.Exit(runApply())
case *cli:
os.Exit(runCLI(*yes))
case *tray:
// Nothing to show without the desktop part
if haveGUI() {
execGUI()
}
default:
if haveGUI() && haveDisplay() {
execGUI()
}
os.Exit(runCLI(*yes))
}
}
// guiPath is the window and tray icon program from das-tools.
const guiPath = "/usr/libexec/das/das-update-gui"
func haveGUI() bool {
_, err := os.Stat(guiPath)
return err == nil
}
func haveDisplay() bool {
return os.Getenv("DISPLAY") != "" || os.Getenv("WAYLAND_DISPLAY") != ""
}
// execGUI replaces this process with the graphical part, passing the
// arguments on. It returns only when that fails.
func execGUI() {
err := syscall.Exec(guiPath, append([]string{guiPath}, os.Args[1:]...), os.Environ())
fmt.Fprintln(os.Stderr, "das-update:", err)
}
type markReporter struct{ w io.Writer }
func (r markReporter) Step(title string) { fmt.Fprintln(r.w, update.MarkStep+title) }
func (r markReporter) Output() io.Writer { return r.w }
func (r markReporter) Snapshot(n int) { fmt.Fprintf(r.w, "%s%d\n", update.MarkSnapshot, n) }
func runApply() int {
if os.Geteuid() != 0 {
fmt.Fprintln(os.Stderr, "das-update: "+i18n.T("update.needroot"))
return 1
}
// Losing the window must not stop xbps halfway through a transaction.
signal.Ignore(syscall.SIGHUP, syscall.SIGPIPE)
r := markReporter{os.Stdout}
res, err := update.System().Apply(context.Background(), r)
if err != nil {
fmt.Println(update.MarkError + err.Error())
return 1
}
if len(res.Updates) == 0 {
fmt.Println(update.MarkUpToDate)
} else if res.Reboot {
fmt.Println(update.MarkReboot)
}
return 0
}
type cliReporter struct{}
func (cliReporter) Step(title string) { fmt.Printf("\n==> %s\n", title) }
func (cliReporter) Output() io.Writer { return os.Stdout }
func (cliReporter) Snapshot(n int) { fmt.Println(i18n.T("update.snapshot", n)) }
func runCLI(yes bool) int {
if !yes {
fmt.Println(i18n.T("update.checking"))
updates, err := xbps.Check(context.Background())
if err != nil {
fmt.Fprintln(os.Stderr, i18n.T("update.checkfailed", err))
return 1
}
if len(updates) == 0 {
fmt.Println(i18n.T("update.uptodate"))
return 0
}
printUpdates(os.Stdout, updates)
if !confirm(os.Stdin) {
fmt.Println(i18n.T("update.cancelled"))
return 1
}
}
if os.Geteuid() != 0 {
return sudo()
}
signal.Ignore(syscall.SIGHUP)
res, err := update.System().Apply(context.Background(), cliReporter{})
fmt.Println()
switch {
case err != nil:
fmt.Fprintf(os.Stderr, "%s %v\n", i18n.T("update.failed"), err)
case len(res.Updates) == 0:
fmt.Println(i18n.T("update.uptodate"))
default:
fmt.Println(i18n.T("update.done"))
if res.Reboot {
fmt.Println(i18n.T("update.reboot"))
}
}
if res.Snapshot > 0 && len(res.Updates) > 0 {
fmt.Println(i18n.T("update.rollback", res.Snapshot, res.Snapshot))
}
if err != nil {
return 1
}
return 0
}
// sudo runs the confirmed update again as root.
func sudo() int {
self, err := os.Executable()
if err == nil {
var path string
path, err = exec.LookPath("sudo")
if err == nil {
err = syscall.Exec(path, []string{"sudo", self, "--cli", "-y", "--lang", system.Lang()}, os.Environ())
}
}
fmt.Fprintf(os.Stderr, "das-update: %s: %v\n", i18n.T("update.needroot"), err)
return 1
}
func printUpdates(w io.Writer, updates []xbps.Update) {
fmt.Fprintln(w, i18n.T("update.available"))
width := 0
for _, u := range updates {
width = max(width, len(u.Name))
}
for _, u := range updates {
switch {
case u.Action == "remove":
fmt.Fprintf(w, " %-*s %s → −\n", width, u.Name, u.OldVersion)
case u.OldVersion == "":
fmt.Fprintf(w, " %-*s + %s\n", width, u.Name, u.NewVersion)
default:
fmt.Fprintf(w, " %-*s %s → %s\n", width, u.Name, u.OldVersion, u.NewVersion)
}
}
fmt.Fprintln(w, i18n.Plural("update.count", len(updates)))
}
// confirm accepts Enter, y/yes and the Ukrainian т/так.
func confirm(in io.Reader) bool {
fmt.Print(i18n.T("update.confirm"))
line, err := bufio.NewReader(in).ReadString('\n')
if err != nil && line == "" {
return false
}
switch strings.ToLower(strings.TrimSpace(line)) {
case "", "y", "yes", "т", "так":
return true
}
return false
}
@@ -0,0 +1,137 @@
package main
import (
"errors"
"fmt"
"os"
"os/exec"
"os/user"
"path/filepath"
"regexp"
"sort"
"strings"
)
var localeNames = map[string]string{
"uk_UA": "Українська",
"en_US": "English (US)",
"en_GB": "English (UK)",
"de_DE": "Deutsch",
"pl_PL": "Polski",
}
// availableLocales lists the built UTF-8 locales as LANG values, e.g. uk_UA.UTF-8.
func availableLocales() []string {
out, err := exec.Command("locale", "-a").Output()
if err != nil {
return nil
}
return parseLocales(string(out))
}
func parseLocales(out string) []string {
var locales []string
for _, l := range strings.Fields(out) {
base, codeset, ok := strings.Cut(l, ".")
if !ok || !strings.EqualFold(strings.ReplaceAll(codeset, "-", ""), "utf8") || !strings.Contains(base, "_") {
continue
}
locales = append(locales, base+".UTF-8")
}
sort.Strings(locales)
return locales
}
func currentLocale() string {
lang := os.Getenv("LANG")
base, _, _ := strings.Cut(lang, ".")
return base + ".UTF-8"
}
func localeName(locale string) string {
base, _, _ := strings.Cut(locale, ".")
if name, ok := localeNames[base]; ok {
return name
}
return base
}
var objectPath = regexp.MustCompile(`object path "([^"]+)"`)
// setLanguage makes LightDM start the next session with the locale. LightDM
// takes it from AccountsService when that runs, and from ~/.dmrc otherwise,
// so both are set.
func setLanguage(locale string) error {
errDmrc := writeDmrc(locale)
errAccounts := setAccountsLanguage(locale)
if errDmrc != nil && errAccounts != nil {
return errors.Join(errDmrc, errAccounts)
}
return nil
}
func setAccountsLanguage(locale string) error {
u, err := user.Current()
if err != nil {
return err
}
out, err := exec.Command("dbus-send", "--system", "--print-reply", "--dest=org.freedesktop.Accounts",
"/org/freedesktop/Accounts", "org.freedesktop.Accounts.FindUserByName", "string:"+u.Username).Output()
if err != nil {
return fmt.Errorf("AccountsService: %w", err)
}
m := objectPath.FindSubmatch(out)
if m == nil {
return fmt.Errorf("AccountsService: no user object in %q", out)
}
if out, err := exec.Command("dbus-send", "--system", "--print-reply", "--dest=org.freedesktop.Accounts",
string(m[1]), "org.freedesktop.Accounts.User.SetLanguage", "string:"+locale).CombinedOutput(); err != nil {
return fmt.Errorf("AccountsService: %w: %s", err, strings.TrimSpace(string(out)))
}
return nil
}
// writeDmrc sets Language in the [Desktop] section of ~/.dmrc.
func writeDmrc(locale string) error {
path := filepath.Join(os.Getenv("HOME"), ".dmrc")
data, err := os.ReadFile(path)
if err != nil && !os.IsNotExist(err) {
return err
}
return os.WriteFile(path, []byte(setDmrcLanguage(string(data), locale)), 0o644)
}
func setDmrcLanguage(data, locale string) string {
lines := strings.Split(strings.TrimRight(data, "\n"), "\n")
if data == "" {
lines = nil
}
section, inserted := -1, false
for i, l := range lines {
t := strings.TrimSpace(l)
switch {
case t == "[Desktop]":
section = i
case strings.HasPrefix(t, "["):
if section >= 0 && !inserted {
lines = append(lines[:i], append([]string{"Language=" + locale}, lines[i:]...)...)
inserted = true
}
section = -1
case section >= 0 && strings.HasPrefix(t, "Language="):
lines[i] = "Language=" + locale
inserted = true
}
if inserted {
break
}
}
switch {
case inserted:
case section >= 0:
lines = append(lines, "Language="+locale)
default:
lines = append(lines, "[Desktop]", "Language="+locale)
}
return strings.Join(lines, "\n") + "\n"
}
@@ -0,0 +1,29 @@
package main
import (
"reflect"
"testing"
)
func TestParseLocales(t *testing.T) {
got := parseLocales("C\nC.utf8\nPOSIX\nuk_UA.utf8\nen_US.UTF-8\nde_DE.iso88591\n")
want := []string{"en_US.UTF-8", "uk_UA.UTF-8"}
if !reflect.DeepEqual(got, want) {
t.Errorf("parseLocales = %q, want %q", got, want)
}
}
func TestSetDmrcLanguage(t *testing.T) {
tests := []struct{ in, want string }{
{"", "[Desktop]\nLanguage=en_US.UTF-8\n"},
{"[Desktop]\nSession=xfce\n", "[Desktop]\nSession=xfce\nLanguage=en_US.UTF-8\n"},
{"[Desktop]\nLanguage=uk_UA.UTF-8\nSession=xfce\n", "[Desktop]\nLanguage=en_US.UTF-8\nSession=xfce\n"},
{"[Desktop]\nSession=xfce\n[Other]\nLanguage=x\n", "[Desktop]\nSession=xfce\nLanguage=en_US.UTF-8\n[Other]\nLanguage=x\n"},
{"[Other]\nKey=1\n", "[Other]\nKey=1\n[Desktop]\nLanguage=en_US.UTF-8\n"},
}
for _, tt := range tests {
if got := setDmrcLanguage(tt.in, "en_US.UTF-8"); got != tt.want {
t.Errorf("setDmrcLanguage(%q) =\n%q\nwant\n%q", tt.in, got, tt.want)
}
}
}
@@ -0,0 +1,334 @@
// Command das-welcome is the Das OS first-login window: interface language,
// home folder names, updates, settings and help.
//
// das-welcome show the window
// das-welcome --autostart show it only if the user did not turn that off
package main
import (
"context"
"flag"
"fmt"
"html"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/gotk3/gotk3/glib"
"github.com/gotk3/gotk3/gtk"
"dasos/tools/internal/i18n"
"dasos/tools/internal/system"
"dasos/tools/internal/userdirs"
)
const (
appID = "ua.org.das.Welcome"
helpURL = "https://git.das.org.ua/das/Das-OS"
)
func main() {
autostart := flag.Bool("autostart", false, "show the window only if it is enabled at login")
flag.Parse()
if *autostart && !showAtLogin() {
return
}
app, err := gtk.ApplicationNew(appID, glib.APPLICATION_FLAGS_NONE)
if err != nil {
fmt.Fprintln(os.Stderr, "das-welcome:", err)
os.Exit(1)
}
var win *gtk.ApplicationWindow
app.Connect("activate", func() {
if win != nil {
win.Present()
return
}
win, err = newWindow(app)
if err != nil {
fmt.Fprintln(os.Stderr, "das-welcome:", err)
app.Quit()
return
}
win.ShowAll()
})
os.Exit(app.Run(nil))
}
// settingsFile records whether the window opens at login. Without it the
// window opens once: at the first login of a user (or every live session).
func settingsFile() string {
dir := os.Getenv("XDG_CONFIG_HOME")
if dir == "" {
dir = filepath.Join(os.Getenv("HOME"), ".config")
}
return filepath.Join(dir, "das", "welcome")
}
func showAtLogin() bool {
data, err := os.ReadFile(settingsFile())
if err != nil {
return true
}
return strings.TrimSpace(string(data)) == "autostart=true"
}
func setShowAtLogin(on bool) error {
path := settingsFile()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
return os.WriteFile(path, []byte(fmt.Sprintf("autostart=%v\n", on)), 0o644)
}
// builder collects the first error of a chain of gotk3 constructors.
type builder struct{ err error }
func (b *builder) check(err error) {
if b.err == nil {
b.err = err
}
}
func (b *builder) label(text string, wrap bool) *gtk.Label {
l, err := gtk.LabelNew(text)
b.check(err)
if err == nil {
l.SetXAlign(0)
l.SetLineWrap(wrap)
}
return l
}
func (b *builder) button(text string, onClick func()) *gtk.Button {
btn, err := gtk.ButtonNewWithLabel(text)
b.check(err)
if err == nil {
btn.SetVAlign(gtk.ALIGN_CENTER)
btn.Connect("clicked", onClick)
}
return btn
}
// row is one entry of the list: icon, title, description and controls.
func (b *builder) row(icon, title, desc string, controls ...gtk.IWidget) (*gtk.Box, *gtk.Label) {
box, err := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 14)
b.check(err)
img, err := gtk.ImageNewFromIconName(icon, gtk.ICON_SIZE_DND)
b.check(err)
text, err := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 2)
b.check(err)
if b.err != nil {
return box, nil
}
img.SetPixelSize(32)
t := b.label("", false)
t.SetMarkup("<b>" + html.EscapeString(title) + "</b>")
d := b.label(desc, true)
if style, err := d.GetStyleContext(); err == nil {
style.AddClass("dim-label")
}
text.PackStart(t, false, false, 0)
text.PackStart(d, false, false, 0)
box.PackStart(img, false, false, 0)
box.PackStart(text, true, true, 0)
for _, c := range controls {
box.PackEnd(c, false, false, 0)
}
return box, d
}
func newWindow(app *gtk.Application) (*gtk.ApplicationWindow, error) {
b := &builder{}
win, err := gtk.ApplicationWindowNew(app)
if err != nil {
return nil, err
}
win.SetTitle(i18n.T("welcome.title"))
win.SetIconName("das-logo")
win.SetDefaultSize(640, -1)
win.SetPosition(gtk.WIN_POS_CENTER)
outer, err := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 16)
b.check(err)
if b.err != nil {
return nil, b.err
}
outer.SetMarginStart(24)
outer.SetMarginEnd(24)
outer.SetMarginTop(24)
outer.SetMarginBottom(18)
header, err := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 16)
b.check(err)
logo, err := gtk.ImageNewFromIconName("das-logo", gtk.ICON_SIZE_DIALOG)
b.check(err)
titles, err := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 4)
b.check(err)
if b.err != nil {
return nil, b.err
}
logo.SetPixelSize(72)
title := b.label("", false)
title.SetMarkup(`<span size="xx-large" weight="bold">` + html.EscapeString(i18n.T("welcome.title")) + "</span>")
titles.SetVAlign(gtk.ALIGN_CENTER)
titles.PackStart(title, false, false, 0)
titles.PackStart(b.label(i18n.T("welcome.subtitle"), true), false, false, 0)
header.PackStart(logo, false, false, 0)
header.PackStart(titles, true, true, 0)
outer.PackStart(header, false, false, 0)
list, err := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 14)
b.check(err)
add := func(row *gtk.Box) {
if sep, err := gtk.SeparatorNew(gtk.ORIENTATION_HORIZONTAL); err == nil {
list.PackStart(sep, false, false, 0)
}
list.PackStart(row, false, false, 0)
}
if system.IsLive() {
install := b.button(i18n.T("welcome.install"), func() { launch("das-install") })
if style, err := install.GetStyleContext(); err == nil {
style.AddClass("suggested-action")
}
row, _ := b.row("das-logo", i18n.T("welcome.install"), i18n.T("welcome.install.desc"), install)
add(row)
}
if row := languageRow(b); row != nil {
add(row)
}
if row := foldersRow(b); row != nil {
add(row)
}
if !system.IsLive() {
row, _ := b.row("software-update-available", i18n.T("welcome.update"), i18n.T("welcome.update.desc"),
b.button(i18n.T("welcome.update.open"), func() { launch("das-update") }))
add(row)
}
row, _ := b.row("preferences-desktop", i18n.T("welcome.settings"), i18n.T("welcome.settings.desc"),
b.button(i18n.T("welcome.open"), func() { launch("xfce4-settings-manager") }))
add(row)
row, _ = b.row("help-browser", i18n.T("welcome.help"), i18n.T("welcome.help.desc"),
b.button(i18n.T("welcome.open"), func() { launch("xdg-open", helpURL) }))
add(row)
outer.PackStart(list, false, false, 0)
footer, err := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 12)
b.check(err)
autostart, err := gtk.CheckButtonNewWithLabel(i18n.T("welcome.autostart"))
b.check(err)
if b.err != nil {
return nil, b.err
}
// The window opens by itself only once unless the user asks for more.
autostart.SetActive(false)
if _, err := os.Stat(settingsFile()); err == nil {
autostart.SetActive(showAtLogin())
} else if err := setShowAtLogin(false); err != nil {
fmt.Fprintln(os.Stderr, "das-welcome:", err)
}
autostart.Connect("toggled", func() {
if err := setShowAtLogin(autostart.GetActive()); err != nil {
fmt.Fprintln(os.Stderr, "das-welcome:", err)
}
})
footer.PackStart(autostart, false, false, 0)
footer.PackEnd(b.button(i18n.T("welcome.close"), func() { win.Close() }), false, false, 0)
outer.PackEnd(footer, false, false, 0)
win.Add(outer)
return win, b.err
}
func launch(name string, args ...string) {
if err := exec.Command(name, args...).Start(); err != nil {
fmt.Fprintln(os.Stderr, "das-welcome:", err)
}
}
// languageRow offers the UTF-8 locales the system has built. It returns nil
// when there is nothing to choose from.
func languageRow(b *builder) *gtk.Box {
locales := availableLocales()
if len(locales) < 2 {
return nil
}
combo, err := gtk.ComboBoxTextNew()
b.check(err)
if err != nil {
return nil
}
current := currentLocale()
for _, l := range locales {
combo.Append(l, localeName(l))
}
if !combo.SetActiveID(current) {
combo.SetActive(0)
}
combo.SetVAlign(gtk.ALIGN_CENTER)
var desc *gtk.Label
apply := b.button(i18n.T("welcome.apply"), func() {
if err := setLanguage(combo.GetActiveID()); err != nil {
desc.SetText(i18n.T("welcome.error", err))
return
}
desc.SetText(i18n.T("welcome.lang.done"))
})
row, d := b.row("preferences-desktop-locale", i18n.T("welcome.lang"), i18n.T("welcome.lang.desc"), apply, combo)
desc = d
return row
}
// foldersRow offers to rename home folders that kept English names.
func foldersRow(b *builder) *gtk.Box {
home := os.Getenv("HOME")
renames, err := pendingRenames(home)
if err != nil {
fmt.Fprintln(os.Stderr, "das-welcome:", err)
return nil
}
if len(renames) == 0 {
return nil
}
names := make([]string, len(renames))
for i, r := range renames {
names[i] = filepath.Base(r.From)
}
var desc *gtk.Label
var rename *gtk.Button
rename = b.button(i18n.T("welcome.dirs.rename"), func() {
if err := userdirs.Apply(home, renames); err != nil {
desc.SetText(i18n.T("welcome.error", err))
return
}
desc.SetText(i18n.T("welcome.dirs.done"))
rename.SetSensitive(false)
})
row, d := b.row("folder", i18n.T("welcome.dirs"), i18n.T("welcome.dirs.desc", strings.Join(names, ", ")), rename)
desc = d
return row
}
func pendingRenames(home string) ([]userdirs.Rename, error) {
data, err := os.ReadFile(userdirs.ConfigFile(home))
if os.IsNotExist(err) {
return nil, nil
}
if err != nil {
return nil, err
}
ctx := context.Background()
english, err := userdirs.Defaults(ctx, "C.UTF-8")
if err != nil {
return nil, err
}
localized, err := userdirs.Defaults(ctx, "")
if err != nil {
return nil, err
}
return userdirs.Plan(home, userdirs.Parse(string(data)), english, localized), nil
}

Some files were not shown because too many files have changed in this diff Show More