Zenit Linux to dystrybucja Linuksa budowana od zera, uzależniona od GNU (libc, toolchain itd.), ale zastępująca klasyczne narzędzia coreutils własnymi, nowoczesnymi odpowiednikami — a docelowo także własnym bootloaderem i systemem init. Można podczas instalacji wybrac zamiast ekosystemu zenit base ekosystem gnu.
Zamiast klonować coreutils 1:1, Zenit pisze każde narzędzie od nowa,
w języku dobranym do jego charakteru, z naciskiem na:
- czytelne, kolorowe komunikaty błędów,
- bezpieczniejsze zachowania domyślne (np.
dldomyślnie przenosi do kosza, aarodrzuca wpisy archiwum ze ścieżkami bezwzględnymi lub..), - nowoczesny, spójny interfejs CLI (
--help,--version, długie i krótkie flagi), - komponenty systemowe (
zboot,zsrv,zesh) podzielone na małe, jednoznacznie odpowiedzialne moduły zamiast monolitycznych plików.
Cały projekt jest na wczesnym etapie 0.1 — każdy komponent ma numer wersji
0.1.0 niezależnie od tego, ile funkcji już zaimplementowano.
Wszystkie narzędzia CLI są napisane w Crystalu. Powłoka zesh oraz
komponenty systemowe (zboot, zsrv) są w Nim.
| Narzędzie | Zastępuje | Status | Opis |
|---|---|---|---|
zesh |
bash |
działa (Nim) | powłoka: potoki, job control, $(...) |
about |
uname |
działa | informacje o systemie |
cr |
mkdir |
działa | tworzenie katalogów |
dl |
rm |
działa | usuwanie plików/katalogów (z koszem) |
mk |
touch |
działa | tworzenie plików / aktualizacja czasu |
ow |
chown |
działa | zmiana właściciela |
gr |
chgrp |
działa | zmiana grupy |
pm |
chmod |
działa | zmiana uprawnień |
rm |
mv |
działa | przenoszenie / zmiana nazwy |
echo |
echo |
działa | wypisywanie tekstu (-n, -e) |
sp |
ls |
szkielet+ | listowanie: kolory, -R, sortowanie po rozmiarze |
kp |
cp |
szkielet | kopiowanie plików/katalogów |
wp |
cat |
szkielet | wypisywanie zawartości plików |
sz |
grep |
szkielet+ | wyszukiwanie: kontekst -A/-B/-C, kolorowanie |
zn |
find |
szkielet+ | wyszukiwanie: --exec, filtry rozmiaru/czasu |
lb |
wc |
szkielet | liczenie linii / słów / bajtów |
wz |
ln |
szkielet | dowiązania twarde i symboliczne |
pr |
ps |
szkielet | listowanie procesów (z /proc) |
df |
df |
szkielet | zajętość miejsca na dyskach (statvfs) |
du |
du |
szkielet | zajętość katalogów (rekurencyjnie) |
zb |
kill |
szkielet | wysyłanie sygnałów do procesów ("zabij") |
fr |
head/tail |
szkielet | podgląd początku/końca pliku, -f ("fragment") |
so |
sort |
szkielet | sortowanie linii (leksykograficznie/numerycznie) |
un |
uniq |
szkielet | usuwanie sąsiadujących duplikatów linii |
ar |
tar |
działa (ustar) | archiwizacja, format ustar + gzip (-z) |
gdz |
which |
szkielet | lokalizacja polecenia w $PATH ("gdzie") |
en |
env |
szkielet+ | zmienne środowiskowe, -i (czyste środowisko) |
id |
id |
szkielet+ | UID/GID z rozwiązywaniem nazw (getpwuid/getgrgid) |
kt |
whoami |
szkielet+ | nazwa użytkownika przez getpwuid(getuid()) ("kto") |
hn |
hostname |
szkielet | odczyt nazwy hosta |
ro |
diff |
szkielet | porównywanie plików tekstowych (LCS) ("różnice") |
xa |
xargs |
szkielet | budowanie i uruchamianie poleceń z stdin |
zdb |
gdb |
szkielet+ | debugger ptrace(2) z symbolami ELF i deasemblacją |
pf |
printf |
szkielet | formatowane wypisywanie (%s/%d/%f) |
wm |
free |
szkielet | zajętość RAM/swap z /proc/meminfo ("wolna pamięć") |
up |
uptime |
szkielet | czas działania i obciążenie systemu |
ni |
nice |
szkielet | uruchomienie polecenia ze zmienionym priorytetem |
36 narzędzi łącznie. „Szkielet” oznacza gotowy interfejs CLI i podstawową
ścieżkę działania z opisanymi w kodzie brakami (TODO); „szkielet+”
oznacza, że dodatkowo zaimplementowano część zaawansowanych opcji.
Historia:
cr,dlimkbyły pierwotnie zapisane jako pliki.crzawierające kod źródłowy Nim (pomyłka nazewnicza). Zostały przepisane na właściwy Crystal, zachowując pełną funkcjonalność oryginału.
Uwaga:
echozostało dodane jako uzupełnienie luki — wcześniejsze wersje repo i dokumentacji zakładały jego istnienie (np. w przykładachzesh), ale nikt nigdy go nie napisał.zeshnie ma wbudowanegoecho— deleguje do zewnętrznego programu w$PATH, tak jak każda klasyczna powłoka uniksowa.
Ten etap celuje wyłącznie w UEFI. Wsparcie dla klasycznego BIOS jest
świadomie odłożone i ma trafić jako osobny plugin implementujący
formalny interfejs BootBackend (patrz bootloader/zbootpkg/backend.nim).
bootloader/
├── zboot.nim # punkt wejścia UEFI (efiMain) — spina moduły
└── zbootpkg/
├── uefi_types.nim # struktury UEFI (System Table, Boot Services, protokoły plików)
├── console.nim # wypisywanie tekstu (ConOut), panic()
├── memory.nim # GetMemoryMap z pełną pętlą ponawiania (BUFFER_TOO_SMALL)
├── filesystem.nim # odczyt obrazu jądra z ESP, GetInfo -> dokładny rozmiar bufora
├── elf.nim # parsowanie ELF64 + kopiowanie segmentów do fizycznej bazy
├── graphics.nim # Graphics Output Protocol — framebuffer dla jądra
├── paging.nim # WŁASNE tablice stron: identity map (2 MiB) + jądro (4 KiB, per-segment R/W/X)
├── handoff.nim # BootInfo, przełączenie stosu i skok do jądra
└── backend.nim # formalny interfejs BootBackend (plugin BIOS w przyszłości)
Higher-half + uprawnienia per-segment: zbootpkg/elf wyznacza
rozpiętość segmentów PT_LOAD, alokuje spójny region fizyczny
(AllocatePages) i kopiuje tam segmenty zachowując ich względne
odległości z linkowania. zbootpkg/paging buduje WŁASNE tablice stron
(PML4/PDPT/PD/PT): identity mapping niskiej pamięci stronami 2 MiB (dla
buforów zboot) oraz każdy segment jądra mapowany OSOBNO stronami 4 KiB
z uprawnieniami odczytanymi z p_flags ELF — sekcja .text dostaje R+X
(bez W), .data/.bss dostają R+W (bez X), zamiast jednego zbyt
szerokiego mapowania R+W+X dla całego jądra naraz. Przed użyciem bitu NX
włączane jest EFER.NXE (inaczej ustawienie NX przy wyłączonym NXE
powoduje #GP zamiast zablokować wykonywanie). Przełączenie CR3
następuje dopiero po ExitBootServices (przed tym firmware polega na
własnych tablicach stron).
Do zrobienia: wyliczanie identityMapGiB dynamicznie z mapy pamięci
zamiast stałej 4 GiB, wybór trybu graficznego o najwyższej rozdzielczości
przez QueryMode/SetMode.
init-system/
├── zsrv.nim # punkt wejścia — spina moduły
└── zsrvpkg/
├── types.nim # ServiceDef, ServiceRuntime, ResourceLimits, Target
├── state.nim # globalny stan: tabela usług, aktywny target
├── logger.nim # logowanie z rotacją pliku
├── parser.nim # parsowanie *.zsrv (User=, MemoryMax=, CPUQuota=, StopSec=)
├── depgraph.nim # sortowanie topologiczne zależności (After=), algorytm Kahna
├── cgroups.nim # limity zasobów przez cgroups v2 + subtree_control
├── supervisor.nim # start/stop/restart, dropPrivileges, setsid, grupy procesów
├── target.nim # target startowy + przełączanie w locie (/run/zenit/target)
└── eventloop.nim # pętla epoll + signalfd
Grupy procesów i przełączanie targetu w locie (nowość): każda usługa
dostaje własne PGID przez setsid() w procesie potomnym — dzięki temu
SIGTERM/SIGKILL trafiają do całej grupy procesów usługi
(kill(-pid, sygnał)), nie tylko do bezpośredniego procesu potomnego, więc
dzieci uruchomione przez usługę też są poprawnie zatrzymywane. Operator
(albo przyszłe zsrvctl isolate TARGET) może przełączyć target w locie,
bez restartu: echo rescue > /run/zenit/target && kill -HUP 1 — zsrv
odczyta plik przy SIGHUP, zmieni currentTarget i zatrzyma usługi
spoza nowego targetu (wcześniej applyTarget tylko dokładał usługi,
nigdy nie zatrzymywał).
Funkcje: cgroups v2 z subtree_control, logi per-usługa w
/var/log/zenit/<usługa>.log, uruchamianie na innym użytkowniku
(User= → setgid+setuid), dwufazowe zamykanie (SIGTERM →
SIGKILL po StopSec=), pętla zdarzeń na epoll+signalfd.
Testowane w tests/test_depgraph.nim (sortowanie topologiczne) i
tests/test_service_parser.nim (parsowanie dyrektyw .zsrv).
zesh/
├── zesh.nim # punkt wejścia, pętla REPL — spina moduły
└── zeshpkg/
├── state.nim # zmienne, aliasy, historia, ostatni kod wyjścia
├── vars.nim # $NAME/${NAME}/$?/$(polecenie), lokalne vs eksportowane
├── cmdhistory.nim # ~/.zesh_history, odwołania !!/!n
├── lexer.nim # tokenizacja (cudzysłowy, operatory, & w tle, $(...))
├── parser.nim # tokeny -> potoki -> instrukcje
├── jobcontrol.nim # kontrola zadań w tle (&, jobs, fg)
├── builtins.nim # polecenia wbudowane (alias, type, fg, jobs, ...)
├── exec.nim # fork/pipe/dup2/execvp, rozwijanie aliasów
├── interpreter.nim # sekwencje `;`, warunki `&&`/`||`, hook $(...)
└── prompt.nim # budowanie promptu
Substytucja poleceń $(...) (nowość): polecenie1 $(polecenie2 arg)
uruchamia polecenie2 przez pełny cykl runLine (więc obsługuje własne
potoki/przekierowania/wbudowane polecenia), z STDOUT_FILENO tymczasowo
przekierowanym na potok przez dup2 — wynik (bez końcowych znaków nowej
linii, zgodnie z POSIX) trafia w miejsce $(...). Ponieważ moduł
zeshpkg/vars jest niskopoziomowy i nie może bezpośrednio importować
zeshpkg/interpreter (powstałby cykl: vars -> interpreter -> lexer -> vars), podłączenie następuje przez wstrzyknięty hook
(vars.commandSubstitutionHook), ustawiany raz przez
interpreter.setupCommandSubstitution() przy starcie zesh — klasyczny
wzorzec odwrócenia zależności na obejście cyklu importów.
Naprawiony błąd: poprzednia wersja rozwijała niecudzysłowione $NAME
znak po znaku (expandVars($c) w pętli), więc expandVars("$") samo
w sobie nigdy nie widziało kolejnych znaków nazwy zmiennej —
echo $HOME (bez cudzysłowu) nigdy się nie rozwijało, działało tylko
echo "$HOME". Naprawione przez buforowanie ciągłych znaków
niecudzysłowionych w unquotedRun i rozwijanie ich razem, przy granicy
słowa — a nie osobno dla każdego znaku. Przy okazji naprawiono też, że
spacja wewnątrz $(polecenie z argumentami) błędnie kończyła token —
$( jest teraz połykane w całości (licząc zagnieżdżone nawiasy) w
głównej pętli tokenizera, zanim reszta znaków zostanie zinterpretowana.
Uwaga historyczna: moduły
history.nimijobs.nim(analogiczniezsrvpkg/log.nim) zostały przemianowane nacmdhistory.nim,jobcontrol.nimilogger.nim— Nim traktuje moduł i zmienną/proc o identycznej nazwie jako kolizję, co blokowało kompilację.
Testowane w tests/test_lexer.nim (w tym test na $(...) ze spacją
w środku) i tests/test_parser.nim.
Przy pierwszych próbach nimble build na czystym środowisku wyszły na
jaw dwa systematyczne błędy, oba już naprawione i opisane tu, żeby
tłumaczyć, dlaczego kod wygląda tak, a nie inaczej, w kilku miejscach:
- Brak
import std/tables: Nim wymaga BEZPOŚREDNIEGO importustd/tablesw każdym module wołającymhasKey/mgetOrPut/[]na typieTable, nawet jeśli inny zaimportowany moduł eksportuje samą zmienną tego typu. Dotknęło tozeshpkg/vars.nim,builtins.nim,exec.nimorazzsrvpkg/parser.nim,supervisor.nim— wszystkie naprawione. - Przechwytywanie
resultw zagnieżdżonymproc: Nim zabrania zagnieżdżonemuproc(domknięciu) przechwytywania niejawnej zmiennejresultze względów bezpieczeństwa pamięci. Dotknęło tozeshpkg/lexer.tokenizeizeshpkg/parser.splitStatements— obie naprawione przez użycie jawnej zmiennej lokalnej zwracanej na końcu zamiast niejawnegoresult.
zenit-linux/
├── shard.yml # definicje WSZYSTKICH narzędzi Crystal (36 narzędzi)
├── zenit_base.nimble # definicje komponentów Nim (zesh, zboot, zsrv) + zadania test/install
├── build.janet # orkiestrator budowy całości (+ `-- test`, `-- install`)
├── scripts/
│ ├── install.sh # kopiuje zbudowane binaria do PREFIX/bin (domyślnie /usr/local/bin)
│ └── uninstall.sh # usuwa je z powrotem
├── tests/ # testy jednostkowe Nim (uruchamiane przez `nimble test`)
├── spec/ # testy integracyjne Crystal (uruchamiane przez `crystal spec`)
├── .github/workflows/
│ ├── build-all.yml # CI: buduje i pakuje wszystko
│ ├── build-tools.yml # CI: narzędzia CLI (Crystal + zesh)
│ ├── build-bootloader.yml # CI: zboot (UEFI, BOOTX64.EFI, test w QEMU/OVMF)
│ ├── build-init-system.yml # CI: zsrv
│ └── test.yml # CI: testy jednostkowe + integracyjne
├── zesh/ # powłoka (Nim, moduły w zeshpkg/)
├── bootloader/ # bootloader UEFI (Nim, moduły w zbootpkg/)
├── init-system/ # system init / PID 1 (Nim, moduły w zsrvpkg/)
└── tools/ # wszystkie narzędzia CLI (Crystal)
└── <nazwa>/src/<nazwa>.cr
Wymagania: Nim ≥ 2.0, Crystal ≥ 1.10, Janet, GNU toolchain, a dla
bootloadera dodatkowo krzyżowy toolchain UEFI: gcc-mingw-w64-x86-64
(oraz opcjonalnie OVMF + QEMU do testów rozruchu).
# Powłoka zesh (Nim) — kompiluje zesh.nim wraz z modułami z zeshpkg/
nimble buildShell
# System init zsrv (Nim) — kompiluje zsrv.nim wraz z modułami z zsrvpkg/
nimble buildInit
# Bootloader zboot (Nim, UEFI) -> bootloader/BOOTX64.EFI
nimble buildBootloader
# Wszystkie komponenty Nim naraz
nimble buildAll
# Narzędzia CLI (Crystal) — 36 narzędzi zdefiniowanych w shard.yml
shards install
shards build --release
# Albo całość naraz, przez orkiestrator:
janet build.janetNazwa pakietu nimble (zenit_base) celowo używa podkreślnika zamiast
myślnika — Nimble odrzuca myślniki w nazwach pakietów jako nieprawidłowe.
Wynikowe binaria trafiają do katalogu dist/.
# Testy jednostkowe komponentów Nim (parsowanie, sortowanie zależności,
# tokenizacja) — czysta logika, bez potrzeby roota/PID 1/forkowania:
nimble test
# Testy integracyjne narzędzi Crystal — wymagają wcześniejszego builda:
shards build --release
crystal spec
# Albo oba naraz przez orkiestrator:
janet build.janet -- testZakres testów jest reprezentatywny, nie wyczerpujący: tests/
pokrywa sortowanie topologiczne zależności zsrv, parsowanie plików
.zsrv, tokenizację (w tym $(...) ze spacją w środku) i parsowanie
poleceń zesh; spec/ pokrywa próbkę narzędzi CLI (cr, mk, dl,
wp, so, un, lb, id, kt, ar, zn, echo).
# Zbuduj wszystko (patrz sekcja Budowanie), potem:
./scripts/install.sh # instaluje do /usr/local/bin
PREFIX=/opt/zenit ./scripts/install.sh # inny prefiks
DESTDIR=/mnt/root ./scripts/install.sh # instalacja do obrazu (budowa pakietów dystrybucji)
# Deinstalacja:
./scripts/uninstall.sh
# Albo przez nimble/janet:
nimble install
janet build.janet -- installinstall.sh tylko kopiuje binaria — nie rejestruje pakietu w
dpkg/rpm/pacman, nie instaluje stron podręcznika (man), i nie konfiguruje
zsrv jako faktyczny PID 1. zboot nie jest instalowany tym
skryptem — trafia na partycję ESP jako \EFI\BOOT\BOOTX64.EFI.
Cały projekt jest udostępniony na licencji Apache License 2.0 — patrz plik LICENSE.
Projekt jest we wczesnej fazie rozwoju (wszystkie komponenty: 0.1.0).
zesh: potoki/przekierowania/warunki, job control, aliasy,$(...)(substytucja poleceń) i naprawione rozwijanie$VARpoza cudzysłowem. Pełna kontrola terminala to TODO.zsrv: epoll/signalfd, sortowanie topologiczne, cgroups v2, grupy procesów (setsid), przełączanie targetu w locie przez/run/zenit/target, dwufazowe zamykanie.zboot: UEFI, higher-half, mapowanie 4 KiB z uprawnieniami R/W/X per-segment ELF iEFER.NXE. Wsparcie BIOS (jako plugin) to kolejny krok.tools/: 36 narzędzi CLI w Crystalu, w tymzdb(symbole ELF + deasemblacja),ar(prawdziwy ustar + gzip, ochrona przed path traversal) i uzupełnioneecho,printf,free,uptime,nice.- Testy i instalacja:
tests/(Nim),spec/(Crystal),scripts/install.sh+scripts/uninstall.sh.
Rejestracja w menedżerach pakietów dystrybucji (dpkg/rpm/pacman) i pełne pokrycie testami wszystkich 36 narzędzi pozostają świadomie poza zakresem obecnego etapu.