Montag, 19. Mai 2025

"A Language A Day" von Andrew Shitow - Buchbesprechung

Der Buchtitel "A Language A Day" kann relativ viel bedeuten, im IT Kontext kann man aber davon ausgehen, dass es im Buch um Programmiersprache(n) geht. Mit dem Untertitel des Buchs wird das dann ziemlich klar: "A brief introduction to 21 programming languages". Auf deutsch: eine kurze Einführung in 21 Programmiersprachen.

In dem vorliegenden, englischsprachigen Buch stellt der Autor Andrew Shitow 21 Programmiersprachen vor. Und, um das schon mal vorwegzunehmen, auf eine sehr gute Art und Weise.

Die 21 Sprachen sind, in alphabetischer Reihenfolge (in der sie auch im Buch vorkommen):
C++, Clojure, Crystal, D, Dart, Elixir, Factor, Go, Hack, Hy, Io, Julia, Kotlin, Lua, Mercury, Nim, OCaml, Raku, Rust, Scala und TypeScript.
Nach welchen Kriterien der Autor die Sprache ausgewählt hat, wird nicht erwähnt - ist für das Buch aber letztendlich auch nicht weiter relevant. Jedenfalls ist die Spannbreite der Sprachen ziemlich weit, es werden eigentlich alle Arten von Sprache behandelt, sowohl was das Paradigma als auch deren Bekanntheitsgrad angeht. Für mich kann ich jedenfalls sagen, dass ich bis zum Lesen des Buchs von vier der 21 Sprachen noch nie etwas gehört hatte und vier weitere Sprachen nur dem Namen nach kannte, ohne jemals irgendwelchen Code gesehen zu haben.

Jeder Sprache ist ein eigenes Kapitel gewidmet, welches ca. ein Dutzend Seiten umfasst. In diesen Kapiteln werden für jede der Programmiersprache, nach einer kurzen Einführung zur Sprache selbst, dieselben Aufgaben programmtechnisch umgesetzt: Ausgabe von "Hallo Welt", Berechnung der Fakultät einer zu übergebenden, natürlichen Zahl, Erstellen eines polymorphen Arrays von Objekten sowie Implementierung eines Sleep Sort Algorithmus, mit dem dann 10 Integerwerte sortiert werden. Im Rahmen der Beispiele wird dann auf relevante Themen wie Variablendefinition (inklusive Typisierung, wo notwendig / zutreffen), Definition von Funktion, objektorientiert Programmierung bzw. das, was die Programmiersprache als vergleichbaren Ansatz anbietet, Fehlerbehandlung sowie nebenläufige Programmierung eingegangen. Den Abschluss jedes Kapitels bilden ein paar Links zur weiterführenden Einführung und Lernressourcen für die jeweilige Programmiersprache.

Was der Autor wirklich gut macht, ist, zwar dem Schema zu folgen, aber trotzdem auf die individuellen Ansätze der jeweiligen Sprache eingeht. Dabei wird auch, wenn immer notwendig bzw. sinnvolle, kurze Exkurse in die jeweiligen Spezifika gemacht, um eine der Aufgaben besser umsetzen zu können. Z.B. gibt es bei Rust ein kurzes Unterkapitel zu Macros oder bei Lua ein kurzes Unterkapitel zu Metatables.

Insgesamt ist das Buch gut geschrieben und liest sich sehr flüssig. Jedes Kapitel zu jeder Sprache ist in sich abgeschlossen und lässt sich aufgrund der Länge gut in 15-20min Lesen, wenn man sich die jeweiligen Codebeispiel mit anschaut. Damit sich das Buch bzw. die Kapitel auch für die kurze Lektüre zwischendurch oder am Abend geeignet.

Lernt man mit dem Buch eine der 21 Programmiersprachen? Nein, sicher nicht, dazu sind die Kapitel viel zu kurz. Hilft das Buch, einen kompakten Einblick in die jeweilige Sprache zu bekommen, der eventuell Lust auf mehr macht? Ja, absolut. Ich denke, dass das auch die Intention des Autors ist, den Leser dazu zu motivieren, sich auch einmal - zumindest kurz - mit anderen Programmiersprachen zu beschäftigen, außerhalb derer, die man vielleicht schon kann. Man muss meiner Meinung nach auch nicht wirklich Ahnung von verschiedenen Programmierparadigmen und -sprachen haben, um das Buch zu lesen. Es reicht, wenn man ein bisschen Programmieren kann - in welcher Sprache auch immer - und natürlich ein generelles Interesse an Programmierung und Programmiersprachen hat.

Jedenfalls ist das Buch eines der interessantesten und kurzweiligsten IT-Büchern, die ich in den letzten Jahren zu Programmiersprachen gelesen habe. Für alle, die sich auch nur ansatzweise für verschieden Programmiersprachen interessieren und auch Lust haben, mal in vielen verschieden Richtungen über den Tellerrand zu blicken, ist das Buch eine absolute Empfehlung. Zumal es sich auch ganz hervorragend kapitelweise lässt und es auch egal ist, in welcher Reihenfolge man die Kapitel liegt und ob eventuell auch mal ein paar Tage mehr zwischen der Lektüre von Kapiteln liegen.

Buchdaten:

  • Titel: A Language A Day - A brief introduction to 21 programming languages
  • Autor: Andrew Shitow
  • Verlage: Deep Text
  • Buchart: Taschenbuch
  • ISBN: 979-8344731834
  • Erscheinungsdatum: 28.10.2024 (1. Auflage)
  • Preis: ca. 16 Euro (bei Amazon)

 

Dienstag, 12. Dezember 2023

Django: Übersetzungen von Forms im Template anzeigen

Neulich habe ich zum ersten Mal eines meiner mit Django erstellten Projekt übersetzt. Bis jetzt waren die komplett in englisch gehalten, jetzt habe ich eine deutsche Übersetzung hinzugefügt.

Wie Übersetzungen / Internationalisierung mit Django funktionieren ist ja ausführlich in der Doku beschrieben (Link zur Django 4.2 Doku). Das hat bei mir auch alles einwandfrei funktioniert - nur wurden die Labels von Formularen aus der Datei forms.py im Template nicht übersetzt angezeigt.

Die forms.py sah so aus:

from django import forms
from django.utils.translation import gettext as _


class SelectByDimensionsForm(forms.Form):
    width = forms.IntegerField(label=_('Inner Width, mm'))
    width_tolerance = forms.IntegerField(label=_('tolerance for width, mm'),
                                         initial=20)
    (...)

und das Template so:

{% extends 'laundercalc/base.html' %}
{% load i18n %}

{% block content %}
{% translate "Please enter your data in the following form:" %} (...)
{% csrf_token %} {{ form.as_p }} (...)
(...)

Während der Text aus dem Template übersetzt angezeigt wurde (wie das "Please enter ...") erschienen die Labels des Formular nach wie vor in Englisch. In der Übersetzungsdatei waren die Labels übersetzt hinterlegt.

Die Lösung, wie man die Übersetzung angezeigt bekommt, ist ganz einfach: man muss nur gettext_lazy in der forms.py importieren, also from django.utils.translation import gettext_lazy as _ (statt ...import gettext as _) verwenden.

Dann wird die Übersetzung erst abgerufen, wenn diese wirklich benötigt wird, im gegebenen Fall also, wenn das Template gerendert wird. Und dann wird es auch komplett übersetzt angezeigt.

Weitere Informationen zu gettext_lazy sind auch in der Django Dokumentation zu finden.

Mittwoch, 22. November 2023

REST-API mit Python und hug für Pimoroni Blinkt LED Leiste erstellen

In diesen kleinen Projekt wird mit Hilfe des Python Webframeworks hug eine einfache webbasierte REST-API zum Ansteuern der Blinkt LED-Leiste von Pimoroni für den Raspbery Pi erstellt.

Das ganze hat keinen speziellen Grund, es ist für mich aber eine gute Gelegenheit, mal etwas mit hug zu erstellen. Wollte ich schon länger, es hatte sich bis dato aber keine Gelegenheit ergeben.

Ziel

Ziel des Programmierprojekt ist es, eine webbasierte API / Steuerung für die Blinkt-Leiste zu erstellen, genutzt werden dafür nur GET-Requests. Konkret bedeutet dies, das z.B. über den Aufruf der URL http://ip_adresse:port/color/red alle acht LEDs des Blinkt auf rot zu stellen. Die LEDs der Blinkt Leiste werden von Pimoroni übrigens "Pixel" genannt, dieser Begriff wird auch im weiteren hier verwendet.

Die API soll die folgenden Funktionen bieten:

  • alle Pixel auf eine vordefinierte Farbe stellen
  • Helligkeit für alle Pixel einstellen
  • für jedes der Pixel eine separate Farbe einstellen
  • für jedes Pixel eine separate Farbe einstellen, aber alle acht Farben auf einmal als Parameter übergeben
  • allen Pixeln eine zufällig gewählte Farbe zuweisen
  • Blinkt Leiste ausschalten

Die Farben sollen sich für diese Projekt nicht frei einstellen lassen, sondern es gibt sieben vordefinierte Farben: rot, orange, gelb, grün, blau, indigo, violett sowie weiß. Was den Farben das Regenbogens plus weiß entspricht.

Hardware

Als Hardware kommen eine Raspberry Pi Zero W sowie die Pimoroni Blinkt Leiste zum Einsatz. Die Blinkt Leiste wird direkt auf die GPIO Pins des Raspberry Pi gesteckt. Natürlich lässt sich das Projekt auch mit jedem anderen Raspberry umsetzen, solange dieser die 40 GPIO Pins hat Auf ganz alte Raspis mit nur 28 Pins passt die Leiste nicht.

Pi Zero im Pibow Gehäuse mit aufgesteckter Blinkt LED Leiste

 Software

Als Software kommt zum Einsatz:

  • Raspberry Pi OS. Hier wird die zum Zeitpunkt des Schreibens aktuelle Version "Bookworm" verwendet, das Projekt funktioniert aber auch mit älteren Versionen.
  • venv zum Erstellen eines virtual Environments für Python, in dem das Programm später läuft
  • innerhalb des venvs pip, der Paketmanager für Python
  • das Python Modul für hug, installiert via pip
  • das Python Modul für Blinkt, installiert aus den Paketquellen von Raspberry Pi OS
  • waitress als WSGI-Server, installiert via pip
  • systemd zum Erstellen einer Service Unit zum automatischen Starten des Server. systemd ist bei Raspberry Pi OS standardmäßig an Bord.

Anstellen von waitress kann natürlich auf jeder andere WSGI-Applikationsserver genutzt werden, wie z.B. Gunicorn. Beim Entwickeln bzw. zum Testen kann man auch den Server nutzen, den hug mitbringt - habe ich auch in der Testphase gemacht.

Programm

Vorbereitung

Als erstes wird die benötigte Software installiert und das venv angelegt sowie aktiviert.

Aus den Paketquellen von Raspberry Pi OS wird folgendes installiert:

sudo apt install python3-venv
sudo apt install python3-blinkt

Dann wird ein venv Names "hug_blinkt" angelegt. Ich habe dieses bei mir im Homeverzeichnis im Unterverzeichnis "code" angelegt, so dass der volle absolute Pfad /home/noisefloor/code/hug_blinkt lautet. Von Prinzip her geht aber natürlich auch jedes andere Verzeichnis, für das man ausreichende Rechte hat.

python3 -m venv hug_blinkt --system-site-packages

Die Option --system-site-packages ist deshalb notwendig, weil das Python-Modul für Blinkt via APT systemweit installiert ist, aber systemweite Pakete standardmäßig nicht in ein venv übernommen werden.

Jetzt ins venv wechseln, dieses aktivieren und darin die Python-Module für hug und waitress installieren:

cd hug_blinkt
source bin/activate
pip3 install hug
pip3 install waitress

Nun ist alles bereit, um das Programm zu erstellen und starten.

Quellcode

Der komplette Quellcode für das Programm sieht wie folgt aus:

import random
import hug
import blinkt


COLORS = {'red': (255, 0 , 0),
          'green': (0, 255, 0),
          'blue': (0, 0, 255),
          'yellow': (255, 255, 0),
          'orange': (255, 165, 0),
          'violet': (255, 87, 51),
          'indigo': (75, 0, 130),
          'white': (255, 255, 255),
          }

#define short name for each color, which is the first letter of the color
#as defined in COLORS
#needs to be modified / removed once two colors with the same starting letter
#are defined in COLORS, e.g. if ever purple and pink would be added.
COLORS_SHORT = {key[0]:value for (key, value) in COLORS.items()}

ALL_COLORS = COLORS | COLORS_SHORT

@hug.get('/color/{color}')
@hug.get('/color', examples='color=red')
def set_all_pixels(color: str):
    '''Sets all pixels to the the given color. The color is specified by name,
    e.g. red or green. Color can also be given by shortname, e.g. y for yellow
    or m for magenta.'''
    if not color in ALL_COLORS.keys():
        return {'error': f'{color} is not a valid color. Please choose one of \
{list(ALL_COLORS.keys())}'}
    else:
        rgb_value = ALL_COLORS[color]
        blinkt.set_all(rgb_value[0], rgb_value[1], rgb_value[2])
        blinkt.show()
        return {'success': f'set Pixels to color {color} (RBG: {rgb_value})'}

@hug.get('/color_for_pixel/{pixel}/{color}')
@hug.get('/color_for_pixel', examples='pixel=2&color=magenta')
def set_pixel_to(pixel: hug.types.in_range(1, 9), color: str):
    '''Sets the given Pixel to the given color. Pixels are numbered 1 to 8 from
    left to right. The color is specified by name, e.g. green or blue. Color can
    also be given by shortname, e.g. y for yellow or m for magenta.'''
    if not color in ALL_COLORS.keys():
        return {'error': f'{color} is not a valid color. Please choose one of \
{list(ALL_COLORS.keys())}'}
    else:
        pixel-=1
        rgb_value = ALL_COLORS[color]
        blinkt.set_pixel(pixel, rgb_value[0], rgb_value[1], rgb_value[2])
        blinkt.show()
        return {'success': f'set Pixel {pixel} to color {color} (RBG: {rgb_value})'}

@hug.get('/colors_for_pixels/{colors}')
@hug.get('/colors_for_pixels', examples='colors=green,red,yellow,magenta,white,blue,y,g')
def set_pixels_to(colors: hug.types.delimited_list(',')):
    '''Set each pixel to the given color specified for each Pixel. The colors
    are specified by name,     e.g. blue or yellow. In total eight colors needs
    to be given, one for each Pixel. Colors must     be separated by a comma , .'''
    if len(colors) != 8:
        return {'error': 'wrong length - exactly eight (8) colors separated by \
a comma have to be provided'}
    if not all(color.strip() in ALL_COLORS for color in colors):
        return {'error': f'There is at least one invalid value in {colors}. \
            Please ensure all values are one of {ALL_COLORS.keys()}'}
    _set_color_for_each_pixel(colors)
    return {'success': f'set Pixels 1 to 8 to colors {colors}'}

@hug.get('/brightness/{value}')
@hug.get('/brightness')
def set_brightness(value: hug.types.in_range(1, 100), 
                   examples='value=50'):
    '''Sets the brightness of all Pixels to the given value. Value must be an
    integer between 1 and 99'''
    value = round(value/100, 2)
    blinkt.set_brightness(value)
    blinkt.show()
    return {'success': f'set brightness to: {value}'}

@hug.get('/randomize')
def randomize_colors():
    '''Sets all eight pixels to a randomly chosen color from the color dict.'''
    colors = random.choices(list(COLORS.keys()), k=8)
    _set_color_for_each_pixel(colors)
    return {'success': 'set pixels to randomly chosen colors.'}

@hug.get('/show_state')
def show_state():
    '''Shows the current state of each pixel - current RGB value and brightness.'''
    state = {}
    for pixel in range(blinkt.NUM_PIXELS):
        value = blinkt.get_pixel(pixel)
        state[f'Pixel {pixel}'] = f'color: {value[0:3]}, brightness: {value[3]}' 
    return state

@hug.get('/off')
def leds_off():
    '''Turns off all pixels.'''
    blinkt.clear()
    blinkt.show()
    return {'success': 'turned off all Pixels'}

def _set_color_for_each_pixel(colors):
    '''Helper function to set each pixel to a given color.
    Expects one argument, which has to be an iterable holding the names of exactly
    eight colors.'''
    assert len(colors)==8
    rgb_values = [ALL_COLORS[color] for color in colors]
    for position, rgb_value in enumerate(rgb_values):
        blinkt.set_pixel(position, rgb_value[0], rgb_value[1], rgb_value[2])
    blinkt.show()

set_all_pixels(color: str) bindet die Route color an die Funktion set_all_pixels. Die Route und die daran gebundene Funktion erwarten ein Parameter Namens color, welches auf zwei Wege übergeben werden kann: color?color=red oder color/red.

Im Wörterbuch COLORS werden die erlaubten Farben definiert, was, wie weiter oben bereits erwähnt, die Regenbogenfarben plus weiß sind. Außerdem wird daraus das Wörterbuch COLORS_SHORT abgeleitet. Darin ist definiert, dass auch nur der Anfangsbuchstabe jeder Farbe als Farbangabe genutzt werden darf, also z.B. y für yellow. Alle Funktionen, denen ein oder mehrere Farben übergeben werden, prüfen, ob die Farbangabe bzw. die Farbangaben gültig sind, sprich im Wörterbuch ALL_COLORS vorkommen.

Alle Funktionen, die an eine Route gebunden sind, liefern als Antwort JSON zurück. Dies ist der Standard von hug. Das Wörterbuch nach dem return wird von hug in ein JSON-Objekt umgewandelt und mit dem passenden Content-Type Header versehen. hug kennt außer JSON aber natürlich auch noch andere Ausgabeformate.

Was meines Erachtens bei hug ganz praktisch ist sind die Type Annotations, welche man optional bei den Route angeben kann. Diese können ganz gewöhnlich sein wie bei color: str . Aber hug kennt auch weiterführende, komplexere Annotations. Hier im Programm verwendet werden zwei weitere:
value: hug.types.in_range(1, 100) prüft und stellt sicher, dass für value nur Zahlen zwischen 1 und 99 akzeptiert werden.
colors: hug.types.delimited_list(',') stellt sicher, dass für colors nur durch ein Komma separierte Liste von Werten akzeptiert wird, wie z.B. red,yellow,blue. Werte wie red;yellow;blue würden zu einem Fehler führen. Nach erfolgreicher Prüfung wandelt hug die Werte praktischerweise direkt in eine Python-Liste um.
Eine tiefer gehende Erklärung zu den Type Annotations in hug sind in der Doku zu finden.

Programm testen

Zum Testen des Programms benötigt man keinen WSGI Applikationsserver, sondern kann den in hug enthaltenen Server nutzen. Diesen ruft man mit

hug -f name_des_programms.py

auf. Danach ist das Programm bzw. die darüber bereit gestellt Web-API unter der Adresse 127.0.0.1:8000 auf dem Rechner, auf dem das Programm läuft, erreichbar.

Deployment

Das fertige Programm sollte zur produktiven / dauerhaften Nutzung - selbst wenn es nur im Heimnetzwerk ist - über einen WSGI-Server ausgeliefert werden, um einen stabilen Betrieb zu gewährleisten. Wie weiter oben bereits erwähnt nutze ich dafür waitress.

Der Befehl zum Starten von waitress innerhalb des venvs lautet:

waitress-serve --threads=2 --listen=192.168.178.82:8001 blinkt_api:__hug_wsgi__

Die Angaben für --listen müssen natürlich auf die IP-Adresse des Host-Rechners angepasst werden. Ebenso können der Port sowie die Anzahl der Threads geändert werden. Der Einstiegspunkt von hug für den WSGI-Server __hug_wsgi__ erhält man automatisch über das import hug im Skript.

Wer das Programm beim Systemstart automatisch starten will kann dies auf Basis der folgenden systemd Service Unit machen:

[Unit]
Description=Blinkt Server using hug as the backend
After=network-online.target

[Service]
User=DEIN_BENUTZERNAME
WorkingDirectory=/PFAD/ZUM/HUG/VENV
ExecStart=/PFAD/ZUM/HUG/VENV/bin/waitress-serve --listen=192.168.178.117:8001 --threads=2 NAME_DES_SKRIPTS:__hug_wsgi__
Restart=always

[Install]
WantedBy=multi-user.target

Der Pfad zum angelegten venv muss entsprechend eingetragen werden. Da hug, waitress sowie das Python-Modul für Blinkt keine speziellen Rechte brauchen kann das ganze ohne Probleme mit Nutzerrechten laufen, Root-Rechte sind nicht notwendig.

abschließende Worte

Wie schon Eingangs erwähnt habe ich das ganze Projekt primär gestartet, um mal etwas mit hug zu machen, was auch einen halbwegs produktiven Nutzen hat. Die Nutzung von hug ist ziemlich einfach und gradlinig. Ein bisschen komisch ist bei hug nur, dass die Doku auf zwei Webseiten verteilt ist, nämlich https://www.hug.rest/website/learn/ und https://hugapi.github.io/hug/ 

hug kann übrigens nicht nur webbasierte REST-APIs, sondern auch APIs für die Kommandozeile. Und hug hat noch wesentlich mehr Funktionen und Möglichkeiten als die hier gezeigten / genutzten.

Das hier gezeigt Programm ist natürlich auch nicht auf die Nutzung in Kombination mit der Blinkt LED-Leiste beschränkt. Das Prinzip lässt sich leicht auch auf andere Hardware übertragen, für die es ein Python-Modul zu Ansteuerung gibt.

Mit der Blinkt LED Leiste lässt sich übrigens sehr gut und einfach eine DIY Ambilight Beleuchtung hinter einem Monitor, Fernseher oder ähnlichem realisieren. Dazu einfach einen Raspberry Pi Zero W mit aufgesteckter Blinkt LED-Leiste hinter einem Monitor anbringen und die Pixel der Blinkt Leiste über eine Web REST-API wie hier gezeigt ansteuern.

Sonntag, 12. November 2023

GNU Nano Editor unter Windows intstallieren

Neulich war ich im Terminal von Windows unterwegs und wollte mal schnell eine einfache Änderung in einer Textdatei machen. Unter Linux wäre das ein Fall für den Nano Editor gewesen - unter Windows gibt es nach meinem Wissen im Terminal aber keinen vorinstallierten, textbasierten Editor. Also Google befragt, was es so gibt. Und, siehe da, es gibt tatsächlich Nano nicht nur für Linux, sondern auch für Windows.

Installieren lässt sich Nano auch direkt im Terminal über eine Paketverwaltung für Windows namens winget. Das ist unter Windows 11 und den neueren Versionen von Windows 10 normalerweise vorinstalliert. Wenn nicht kann man es einfach nachinstallieren, eine Anleitung und weitere Informationen sind auf der Infoseite von Microsoft zu finden.

Die Installation ist ganz einfach: einfach im Terminal den Befehl

winget install -e --id GNU.Nano 

ausführen und die aktuelle Nanio-Version wird installiert.

Über winget lässt sich auch noch andere Software aus dem GNU Projekt installieren, wie z.B. der Editor Emacs oder der textbasierte Dateimanager Midnight Commander. Eine Übersicht über die installierbare Software des GNU Projekt ist auf dieser Seite zu finden.

Leider ist es mir (bis jetzt) nicht gelungen die Stelle herauszufinden, wo man die Konfigurationsdatei .nanorc ablegen muss, um Nano Konfigurationsoptionen wie automatisches Einrücken, Zeilennummern, Leerzeichen statt Tab usw. mitzugeben.

Nano ist sicherlich nicht der beste Editor für Windows, aber für einen schnellen Edit im Terminal bzw. in der Powershell, ohne diese zu verlassen, gut genug.

Nano in der Windows Powershell




Sonntag, 29. Oktober 2023

ein snap Paket untersuchen

ein snap Paket untersuchen

snap Pakete bestehen aus einer einzelnen Datei mit der Dateiendung .snap . Diese Datei ist technisch gesehen ein SquashFS-Dateisystem, in das alle Dateien, die das snap enthält, verpackt sind.

Was in dem snap Paket enthalten ist kann man unter- / durchsuchen. Je nach dem, ob das snap ins System eingehängt ist oder nicht unterscheidet sich der Weg.

eingehängtes snap Paket untersuchen

Den Inhalt eines snap Pakets, welches ins System eingehängt ist, zu unter- / durchsuchen benötigt keine weiteren Hilfsmittel. Man navigiert einfach zum Einhängepunkt, also /snap . Dann geht man den Ordner des gewünschten snaps, z.B. firefox für das Firefox snap. In diesem Ordner befindet sich immer ein Verzeichnis current , welches immer ein Link auf die aktuelle, installiert, eingehängte Version des snaps ist. Wechselt man in dieses Verzeichnis sieht man alle Dateien und Verzeichnisse, die zum snap gehören. Durch diese Verzeichnisse kann man navigieren, sich Dateien anzeigen lassen usw. Befindet man sich z.B. im Verzeichnis /snap/firefox/current kann man sich im Terminal über

less firefox.desktop

die im snap enthaltene .desktop-Datei anzeigen lassen.

nicht eingehängtes snap Paket untersuchen

Ein snap Paket muss aber nicht zwingend eingehängt sein, um es zu untersuchen. Man kann auch heruntergeladen bzw. nicht eingehängte Pakete untersuchen. Komfortable geht dies, indem man Programme aus den squashfs-tools-ng nutzt.

Vorbereitung: squashfs-tools-ng installieren

Das Paket "squashfs-tools-ng" ist in den Paketquellen einiger Linux-Distribution enthalten. Eine Übersicht findet man auf der Projektseite.

Unter Ubuntu / Debian / Raspberry Pi OS kann man das Paket über

sudo apt install squashfs-tools-ng

installieren.

Inhalt anzeigen lassen

Nach der Installation von "squashfs-tools-ng" steht unter anderem das Programm rdsquashfs zur Verfügung. Mit dessen Hilfe kann man sich den Inhalt eines SquashFS-Dateisystems - und damit auch eines snaps - anzeigen lassen.

Zuerst wechselt man in der Verzeichnis, wo das snap Paket liegt. Unter Ubuntu werden regulär installierte snaps unter /var/lib/snapd/snaps gespeichert. Liegt dort z.B. das snap core22_858.snap kann man sich über

sudo rdsquashfs core22_858.snap -l /

das Wurzelverzeichnis diese snaps anzeigen lassen. Das sudo ist hier notwendig, weil die snaps in /var/lib/snapdsnaps zum Besitzer und zur Gruppe root gehören und die Dateirechte mit 300 restriktiv gesetzt sind. Hätte man ein snap manuell heruntergeladen und z.B. im eigenen Home-Verzeichnis gespeichert, wären das Ausführen des Befehls mit sudo nicht notwendig.

Die Option -l des Programm rdsquashfs gibt an, welches Verzeichnis ausgegeben werden sollte. Möchte man sich z.B. alle ausführbaren Programme ausgeben lassen, die in diesem snap enthalten sind, ruft man den Befehl

sudo rdsquashfs core22_858.snap -l /usr/bin

auf.

Man kann sich mit Hilfe von rdsquashfs auch Dateien aus dem snap ausgeben lasse. Dazu muss man das Programm mit der Option -c gefolgt vom vollen Pfad und Dateinamen aufrufen. Im folgenden Beispiel würde die Datei /etc/systemd/journald.conf aus dem snap aufgelistet:

sudo rdsquashfs core22_858.snap -c /etc/systemd/journald.conf

Man kann sich auch einfach alle Dateien und Verzeichnisse aus dem snap auflisten lassen:

sudo rdsquashfs core22_858.snap -d

rdsquashfs kennt noch einige weitere Optionen, unter anderem zum Entpacken von einzelnen oder allen Dateien aus dem snap Paket. Diese kann man sich über den Aufruf von

rdsquashfs -h
#oder
man rdsquashfs

anzeigen lasssen.

zwei snap Pakete vergleichen

Die "squashfs-tool-ng" beinhalten auch das Programm sqfsdiff mit dem sich der Inhalt von zwei SquashFS-Dateisystemen und damit auch von zwei snap Paketen vergleichen lässt. Im folgenden Beispiel würden zwei Version des core22 snaps, welche im aktuellen Verzeichnis gespeichert sind, vergleichen:

sudo sqfsdiff --old core22_858.snap --new core22_864.snap

Je nach dem, wie viele Dateien und Verzeichnisse im snap Paket sind bzw. sich geändert haben, kann die Ausgabe sehr lang sein. Es gibt verschiedene Arten von Unterschieden:

  • Lautet eine Ausgabezeile regular file ./pfad/zur datei/dateiname differs bedeutet dies, dass die Datei und der Dateipfad in beiden snaps identisch ist, sich aber der Inhalt der Datei verändert hat.
  • Lautet eine Ausgabezeile > /pfad/zur/datei/dateinamebedeutet dies, diese diese Datei mit diesem Dateipfad in der neuen Datei enthalten ist, aber nicht in der alten.
  • Lautet eine Ausgabezeile < /pfad/zur/datei/dateinamebedeutet dies, diese diese Datei mit diesem Dateipfad in der alten Datei enthalten ist, aber nicht in der neue.
  • Lautet eine Ausgabezeile /pfad/zur/datei/dateiname has a different link target bedeutet dies, dass Datei und Dateipfad in beiden snaps enthalten ist, aber sich die Verlinkung innerhalb des SquashFS-Dateisystem geändert hat.

sqfsdiff kennt eine Reihe von Optionen, um die Vergleich einzuschränken. Die Optionen kann man sich über den Aufruf von

sqfsdiff -h
#oder
man sqfsdiff

anzeigen lassen.

Links

snap

  • Startseite der offiziellen Dokumentation zu snaps

squashfs

squashfs-tools-ng

Freitag, 30. Dezember 2022

ein snap ausprobieren / testweise installieren

ein snap testen

Hinweis: das im folgenden beschriebene Vorgehen ist explizit mit Ubuntu 22.04 LTS durchgeführt worden. Da snapd, der Daemon, der für snaps zuständig ist, aber von Canonical für alle unterstützen Ubuntuversionen gleichermaßen gepflegt wird, sollte das Vorgehen auch für andere Ubuntuversion funktionieren. Ebenso sollte das Vorgehen für andere Linuxdistributionen funktionieren, auf denen snapd installiert ist.

snap, das Paketformat von Canonical erlaubt es, ein Programm auszuprobieren, ohne dass das snap komplett installiert werden mus.

Im Vergleich zu einer normalen Installation eines snaps über snap install unterscheidet sich dieses Verfahren in vier Punkten:

  1. Es werden keine Konfigurationsdateien im Homeverzeichnis des Nutzers unter ~/.config angelegt - es werden aber trotzdem Dateien unter ~/snap für das snap angelegt, das man testet.
  2. Das snap wird bei einem snap refresh nicht aktualisiert.
  3. Man könnte Dateien, die zum snap gehören, zur Laufzeit des snaps editieren und diese Änderung würden beim Start des Programms berücksichtigt. Was sich aber nicht ändern lässt ist das Confinement sowie die Connections eines snaps. 1.. Wenn man das Programm über die Kommandozeile startet, erhält man dort gegebenenfalls die Ausgaben des Programms wie Fehler- oder Warnmeldung. Dies kann bei der Fehlersuche hilfreich sein, wenn ein über ein snap installiertes Programm Probleme machen sollte.

Hinweis: Diese Verfahren ist dazu gedacht, Programme auszuprobieren, die noch nicht regulär installiert sind. Möchte man zwei verschiedene Versionen eines snap parallel nutzen sind Hinweis dazu im Blogartikel snaps parallel installieren zu finden.

Vorgehen

Im folgenden wird das Inkscape snap für das Vektorzeichenprogramm Inkscape als Beispiel genutzt. Die snap Version ist 10512. Für andere snaps sind einfach der Name und die Versionsnummer entsprechend anzupassen. Für alle folgenden Schritte gehe ich davon aus, dass man sich in seinem eigenen Homeverzeichnis ~/ befindet.

Die folgenden Befehle müssen alle im Terminal ausgeführt werden, teilweise sind dafür Root-Rechte notwendig.

Herunterladen und testweise einbinden

Zuerst lädt man das snap herunter, ohne es zu installieren, mit Hilfe des Befehls download:

$ snap download inkscape

Dabei werden zwei Dateien heruntergeladen: das snap an sich, in diesem Beispiel namens inkscape_10512.snap und die zugehörige Assertion-Datei (Verifizierungsdatei) inkscape_10512.assert, welche die kryptographischen Schlüssel und Validierungsdaten enthält, die bei einer Installation des snaps geprüft würden. "10512" ist hierbei die Versionsnummer zur Zeit des Downloads aktuelle, stabilen snaps. Der Befehl download kennt auch ein Option für Channels, d.h. man kann auch eine Beta- oder Testversion herunterladen - je nach dem was für das jeweilige snap bereit gestellt wird.

snaps in ein SquashFS Dateisystem verpackt, welches zuerst in ein Verzeichnis entpackt werden muss. Das notwendige Programm Namens unsquashfs ist bei Ubuntu standardmäßig installiert (wenn zuerst nein muss über die Paketverwaltung zuerst das Paket "squashfs-tools" installiert werden):

$ unsquashfs inkscape_10512.snap

Im Verzeichnis ist jetzt ein neues Unterverzeichnis namens squashfs-root angelegt worden, welches das entpackte snap enthält. Bei Interesse kann man durch das Verzeichnis und dessen Unterverzeichnisse navigieren, um zu sehen, wo welche Dateien liegen.

Zum Ausprobieren von Inkscape muss man den folgenden Befehl ausführen:

$ sudo snap try squashfs-root

Auch, wenn man sich im eigenen Homeverzeichnis befindet, sind Root-Rechte notwendig. Anschließend kann Inkscape regulär im Terminal, die GNOME-Shell bzw. Weg der jeweiligen Desktopumgebung den gestartet werden.

Das testweise laufende snap erscheint jetzt auch in der Liste des installierten snaps, auch wenn es nicht richtig installiert ist:

$ snap list

Name             Version                           Revision  Tracking    Herausgeber  Hinweise  
...  
inkscape         1.2.2-b0a8486541-2022-12-01-cust  x1        -           -            try  
...  

Die Revisionsnummer für snap zum Ausprobieren ist x1 und in der Spalte "Hinweis" ist zu sehen, dass das Inkscape snap im try-Modus ist.

Ein so eingebundenes snap wird auch bei einem Systemneustart wieder eingebunden, wie andere, regulär installierte snaps auch. Das deaktivieren / deinstallieren wird im folgenden Abschnitt beschrieben.

snap wieder entfernen

Das testweise installierte snap kann wie ein reguläres snap deinstalliert werden:

$ snap remove inkscape

Danach kann man das Verzeichnis ~/squashfs-root und das Verzeichnis inklusive Unterverzeichnissen ~/snap/inkscape löschen. Des Weiteren kann man die heruntergeladenen Dateien inkscape_10512.snap und inkscape_10512.assert löschen.

Was tun, wenn versehentlich auch ein produktiv genutztes snap ausprobiert wurde?

Hat man - versehentlich oder bewusst - ein snap testweise installiert, welches in einer anderen Version schon regulär installiert war (z.B. das Firefox snap), dann wird regulär installierte gegebenenfalls durch das testweise installierte ersetzt. Dies lässt sich aber einfach rückgängig machen, im folgenden am Beispiel des Firefox snaps gezeigt:

Zuerst prüft man, ob noch eine reguläre Version des snap installiert ist. Dies sollte normalerweise der Fall sein, da immer mindestens eine ältere Version eines snap noch vorhanden ist:

$ snap list firefox --all

...  
firefox  107.0.1-1  2154      latest/stable  mozilla✓     deaktiviert,try  
firefox  108.0b9-1  x1        latest/stable  -            try  
...  

Die Ausgabe zeigt, das es die testweise installierte Version x1 und eine weitere, reguläre Version 2154 auf dem System gibt. Mit den folgenden beiden Befehlen kehrt man zur regulären Version zurück und löscht das snap der Testversion:

$ sudo snap revert firefox $ sudo snap remove firefox --revision=x1

Für das vollständige Entfernen des testweise installierten Version löscht man noch die Dateien wie weiter oben im Abschnitt "snap wieder entfernen" beschrieben.

Dienstag, 27. Dezember 2022

snaps parallel installieren

snaps parallel installieren

Hinweis: das im folgenden beschriebene Vorgehen ist explizit mit Ubuntu 22.04 LTS durchgeführt worden. Da snapd, der Daemon, der für snaps zuständig ist, aber von Canonical für alle unterstützen Ubuntuversionen gleichermaßen gepflegt wird, sollte das Vorgehen auch für andere Ubuntuversion funktionieren. Ebenso sollte das Vorgehen für andere Linuxdistributionen funktionieren, auf denen snapd installiert ist.

snap, das Paketformat von Canonical erlaubt es, mehrere Versionen eines Programms parallel zu installieren. Die Voraussetzung ist dabei, dass die snaps aus verschiedenen Channels stammen. Verschiedene Channels können z.B. eine Extend Support Version eines Programms sein und der reguläre / aktuelle, stabile Release.

Verschiedene Channels sind aber in der Regel nicht die aktuelle, stabile Version ein Programms und die Betaversion, da diese bei vielen / den meisten snaps im selben Track sind und damit nicht parallel installierbar

Warum parallel installieren?

Ob man ein snap in mehreren Version parallel installieren möchte (oder muss), muss man letztenslich für sich selber entschieden. Wirklich nötig ist es - zumindest in meinen Augen - wohl eher selten. Interessant ist es vielleicht für diejenigen, die zu Testzwecken mehrere Versionen eines Programms benötigen, wie z.B. den auch im folgenden als Beispiel verwendeten Firefox in der jeweils aktuellen und ESR-Version.

Risiko einer Parallelinstallation

Das Risiko kann, je nach snap, sein, dass die eine Version des snaps Konfigurationsdateien des anderen snap überschreibt oder ändert, was zu Problemen führen könnte (aber nicht muss).

Vorgehen

Im folgenden wird als Beispiel das Firefox snap aus dem "stable/latest" (=das standardmäßig installiere) und dem "esr/latest" Track/Channel, so dass zwischen der jeweils aktuellsten und aktuellsten ESR (=Extended Support Release) Version gewechselt werden kann. Das Vorgehen ist auf andere snap und andere Channels übertragbar.

Der Wechsel erfolgt über Befehle, die im Terminal eingegeben werden müssen. Zum Wechsel zwischen zwei Versionen dient der Befehl

$ snap switch --channel=TRACK/CHANNEL NAME_DES_SNAPS

Nach dem Ausführen von switch muss immer anschließend noch ein

$ snap refresh

ausgeführt werden. Zum Wechsel auf den stabilen ESR-Track des Firefox führt man die Befehle

$ snap switch --channel=esr/stable firefox $ snap refresh

aus. Da die ESR-Version des Firefox noch nicht installiert ist, wird diese heruntergeladen und aktiviert. Gleichzeitig wird der latest/stable Firefox deaktiviert, aber nicht deinstalliert. Beim nächsten Start des Firefox nutzt man die ESR Version.

Um wieder zum latest/stable Track zurück zu wechseln führt man wieder die beiden Befehle

$ snap switch --channel=lastest/stable firefox $snap refresh

aus.

einen Channel wieder deinstallieren

Möchte man doch nur einen Channel, im folgenden latest/stable nutzen und die Programmversion des anderen deinstallieren, dann wechselt man zuerst in den zu deinstallierenden Channel

$ snap switch --channel=esr/stable firefox $ snap refresh

und deinstalliert dann den Firefox

$ snap remove firefox $ snap switch --channel=latest/stable firefox $ snap refresh

Anschließend muss der Firefox aus dem latest/stable Channel gegebenenfalls noch aktiviert werden. Steht in der Ausgabe von

$ snap list firefox --all

in der Spalte "Hinweise" der Eintrag disabled, dann führt man noch den Befehl

$ snap activate firefox $ snap refresh

aus.

Mittwoch, 8. Juni 2022

Python und Geschwindigkeit auf verschiedenen Intel und AMD CPUs - und einem Apple Bionic Chip

Vor ein paar Jahren gab es hier im Blog einen Post, der die Geschwindigkeit eines Pythonprogramms zum Number-Crunching (in Form eines Primzahlentests) beschriebt (Link zum Artikel).

Ich hatte jetzt kürzliche die Gelegenheit, die Ausführungsgeschwindigkeit auf vier verschiedenen Mobil CPUs verschiedener Hersteller und Generation zu vergleichen, nämlich:

Die Prozessoren sind also eine zum Zeitpunkt des Schreibens des Artikels eine ca. 7 Jahre alte CPU (der Core i5-5200U, erschienen 2015), ein Core i7 Prozessor der 10. Generation (erschienen zweite Jahreshälfte 2019), ein Core i7 Prozessor der 11. Generation (erschienen 2. Jahreshälfte 2021)  sowie die ebenfalls 2021 erschienene Ryzen 7 5700U CPU.

Außerdem habe ich noch einen Test auf einem iPad Air gemacht. Mehr dazu am Ende des Artikels.

Verglichen habe ich nur die Ausführungsgeschwindigkeit des Python-Programms ohne Optimierungen / Beschleunigungen, also ohne PyPy, Cython, JIT Compiler etc.

Als Betriebssystem diente für den Core i5-5200U und den AMD Ryzen 7 5700U Ubuntu 22.04 mit Python 3.10.4 aus den offiziellen Ubuntu Quelle und für die beiden Intel Core i7 Windows 11, ebenfalls mit Python 3.10.4 aus dem Windows App Store, also die Python-Version, die direkt von der PSF bereit gestellt wird.
Das ganze wurde immer bei ruhendem Desktop gemessen, d.h. außer dem Terminal, in dem das Python-Programm läuft, läuft kein weiteres Programm. Es wurde jedes Skript 3x ausgeführt, die weiter unten auf geführten Zeitangabe sind der Mittelwert der Ausführungszeiten.

Als erste habe ich die Ausführungsgeschwindigkeit mit nur einem Prozess gemessen, das Programm sieht wie folgt aus:

import math
from time import time

PRIMES = [
    112272535095293,
    112582705942171,
    112272535095293,
    115280095190773,
    115797848077099,
    1099726899285419,
    777777722155555333,
    777777722155555335,
    9999999900000001,
    2327074306453592351,
    2327074306453592353]

def is_prime(n):
    if n % 2 == 0:
        return False

    sqrt_n = int(math.floor(math.sqrt(n)))
    for i in range(3, sqrt_n + 1, 2):
        if n % i == 0:
            return False
    return True

def main():
    for number in PRIMES:
        prime = is_prime(number)
        print('{} is prime: {}'.format(number, prime))

if __name__ == '__main__':
    start = time()
    main()
    end = time()
    print(f'Time: {end - start}')

Die Ergebnisse sind wie folgt:

  • Core i5-5200U: 204 Sekunden
  • Core i7-10510U: 111 Sekunden
  • Core i7-1165G7: 68 Sekunden
  • Ryzen 7 5700U: 78 Sekunden

Wie zu erwarten war, sind die neueren CPUs deutlich schneller. Interessant ist auch die relative große Unterschied zwischen dem Core i7 10. Generation und 11. Generation, letzterer ist etwas 60% schneller. Hier hat Intel deutlich in Sachen Leistung nachgelegt, zumindest was die Performance von nur einem Kern angeht.
Zweite Beobachtung: im weiter oben verlinkten Blogpost wurde der gleiche Code ebenfalls auf dem gleichen Core i5-5200U ausgeführt, nur damals unter Python 3.6. Die Geschwindigkeit war damals quasi identisch mit der hier gemessenen. Auch wenn Python (bzw. genau genommen die Referenzimplementierung CPython) einige Verbesserungen in Sachen Geschwindigkeit bekommen hat, spielen diese zumindest für dieses Szenario keine Rolle, der Code ist auf der gleichen CPU bzw. dem gleichen Rechner wie damals quasi gleich schnell.

Im zweiten Test wurde das Programm modifiziert, so dass mit Hilfe des concurent.futures Moduls mehrere Prozesses zum Rechnen gestartet. Da die Core i5 5200U CPU und auch die neueren Core i7 CPUs "nur" vier Kerne haben, wurde als Prozessanzahl vier gewählt, damit im Idealfall jeder Prozess auf einem Kern läuft.

Der Code sieht wie folgt aus:

import math
from time import time
import concurrent.futures

WORKERS = 4

PRIMES = [
    112272535095293,
    112582705942171,
    112272535095293,
    115280095190773,
    115797848077099,
    1099726899285419,
    777777722155555333,
    777777722155555335,
    9999999900000001,
    2327074306453592351,
    2327074306453592353]

def is_prime(n):
    if n % 2 == 0:
        return False

    sqrt_n = int(math.floor(math.sqrt(n)))
    for i in range(3, sqrt_n + 1, 2):
        if n % i == 0:
            return False
    return True

def main():
    with concurrent.futures.ProcessPoolExecutor(max_workers=WORKERS) as executor:
        for number, prime in zip(PRIMES, executor.map(is_prime, PRIMES)):
            print('%d is prime: %s' % (number, prime))


if __name__ == '__main__':
    print(f'using {WORKERS} workers for calculation')
    start = time()
    main()
    end = time()
    print(f'Time: {end - start}')

Die Ergebnisse für den Multiprozessansatz sind:

  • Core i5-5200U: 111 Sekunden
  • Core i7-10510U: 89 Sekunden
  • Core i7-1165G7: 50 Sekunden
  • Ryzen 7 5700U: 51 Sekunden

Auch hier ist der Leistungsunterschied zwischen dem Core i7 10. und 11. Generation erheblich, der neuere ist ca. 80% schneller. Der Core i7-1168G7 und der Ryzen 7 5700U sind quasi gleich schnell.

Ein zusätzlicher Test mit dem Ryzen 7 und acht Workern brachte keine weitere Beschleunigung, das Ergebnis war quasi identisch mit dem mit vier Workern. Dies liegt im gegebenen Test aber ziemlich sicher daran, dass die ersten sechs Prüfungen relativ schnell abgeschlossen sind, d.h. man zieht für die restlichen fünf Zahlen keinen echten Nutzen aus mehr Kernen, weil nicht mehr so viele Prozesse laufen.

Festzuhalten bleibt aber auch: natürlich sind neuere Prozessoren schneller, aber bringen (bei weitem) nicht so viel wie der Einsatz von schnelleren Python-Implementierungen und JIT Compilern.
Alleine der simple Einsatz von PyPy statt CPython auf dem "alten" Core i5 Prozessor führt den Code doppelt so schnell aus wie die Core i7 CPU der 11. Generation oder der Ryzen 7 5700U.

Zusätzlich zu den diversen Intel und der AMD CPU habe ich den nicht-parallelen Code auf einem iPad Air 3. Generation, iOS 15.5 und Python 3.6.1 aus der Pythonista ausgeführt. Dieses iPad hat eine A12 Bionic CPU. Der parallelisierte Code funktioniert ja leider nicht unter iOS, weil, zumindest zum Zeitpunkt des Schreibens des Artikels, ein Prozess keine weiteren Prozesse starten darf.

Der A12 Bionic braucht für den obigen Code ca. 104 Sekunden - und ist damit ca. 10% schneller der getestet Intel Core i7 der 10. Generation. Schon ganz ordentlich für einen Prozessor, der ca. ein Jahr vor dem Intel Prozessor erschienen ist, wobei allerdings auch der Takt der leistungsstärkeren Kerne des A13 mit 2,,49 GHz (deutlich) höher ist als die 1,80 GHz Core i7-10510U.