commit 93c024148881b1232ecca645396685834546909d Author: Christian Fraß Date: Fri Jul 12 11:19:40 2024 +0200 [ini] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ff1c21b --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/temp/ +/build/ +**/__pycache__ diff --git a/source/data.json b/source/data.json new file mode 100644 index 0000000..24b7f57 --- /dev/null +++ b/source/data.json @@ -0,0 +1,417 @@ +{ + "meta": { + "author": "Christian Fraß", + "date": "2024-07-11", + "title": "Digitale Infrastruktur für DIE LINKE.", + "contact": { + "email_address": "christian.frass@dielinke-glauchau.de" + } + }, + "services": { + "theory": [ + { + "text": "Wie setzt man einen Web-Dienst mit ausgelagerter Anmeldung auf?", + "image": "services-0" + }, + { + "text": "Zunächst wird eine Datenbank eingerichtet", + "image": "services-1" + }, + { + "text": "Weiterhin wird ein Web-Server benötigt", + "image": "services-2" + }, + { + "text": "Zudem soll ein Authentifizierungs-Dienst zum Einsatz kommen", + "image": "services-3" + }, + { + "text": "… dieser benötigt in der Regel eine Datenbank-Anbindung", + "image": "services-4" + }, + { + "text": "… und soll über den Web-Server von außen erreichbar sein", + "image": "services-5" + }, + { + "text": "Der eigentliche Dienst …", + "image": "services-6" + }, + { + "text": "… benötigt ebenfalls eine Datenbank-Anbindung", + "image": "services-7" + }, + { + "text": "… soll den Authentifizierungs-Dienst nutzen", + "image": "services-8" + }, + { + "text": "… und über den Web-Server angesprochen werden", + "image": "services-9" + } + ], + "practice": { + "example": { + "link": "https://linke.sx", + "label": "linke.sx" + }, + "technologies": { + "pool": { + "debian": { + "name": "Debian GNU/Linux", + "link": "https://www.debian.org/", + "icon": null, + "desc": "Betriebssystem" + }, + "ansible": { + "name": "Ansible", + "link": "https://www.ansible.com/", + "icon": null, + "desc": "IAC-Plattform" + }, + "keepassxc": { + "name": "KeePassXC", + "link": "https://keepassxc.org/", + "icon": null, + "desc": "Passwort-Verwaltung" + }, + "letsencrypt": { + "name": "Let's Encrypt", + "link": "https://letsencrypt.org/", + "icon": null, + "desc": "TLS-Zertifikats-Erstellung" + }, + "inwx": { + "name": "INWX", + "link": "https://www.inwx.de/", + "icon": null, + "desc": "Domänen-Registrar" + } + }, + "order": [ + "debian", + "ansible", + "keepassxc", + "inwx", + "letsencrypt" + ] + }, + "basics": { + "pool": { + "postgresql": { + "name": "PostgreSQL", + "link": "https://www.postgresql.org/", + "icon": null, + "desc": "Datenbank-Server" + }, + "nginx": { + "name": "nginx", + "link": "https://nginx.org/", + "icon": null, + "desc": "Web-Server und Lastenverteiler" + }, + "authelia": { + "name": "Authelia", + "link": "https://www.authelia.com/", + "icon": null, + "desc": "Auth-Server" + } + }, + "order": [ + "postgresql", + "nginx", + "authelia" + ] + }, + "concrete_present": { + "pool": { + "synapse": { + "name": "Synapse", + "link": "https://github.com/element-hq/synapse", + "icon": "https://matrix.org/images/matrix-favicon.svg", + "desc": "Chat-Server" + }, + "element": { + "name": "Element", + "link": "https://element.io/", + "icon": "https://element.io/images/webclip.png", + "desc": "Chat-Client für Browser" + }, + "hedgedoc": { + "name": "Hedgedoc", + "link": "https://hedgedoc.org/", + "icon": "https://informatik-box.de/images/hedgedoc.png", + "desc": "Notizen" + }, + "gitlab": { + "name": "GitLab", + "link": "https://about.gitlab.com/", + "icon": "https://about.gitlab.com/nuxt-images/ico/favicon-192x192.png", + "desc": "Code-Management und Aufgabenverwaltung", + "stat": "present" + }, + "forgejo": { + "name": "Forgejo", + "link": "https://forgejo.org/", + "icon": "https://forgejo.org/favicon.png", + "desc": "Code-Management und Aufgabenverwaltung" + }, + "vikunja": { + "name": "Vikunja", + "link": "https://vikunja.io/", + "icon": "https://vikunja.io/favicon.svg", + "desc": "Aufgabenverwaltung und Kalender" + }, + "dokuwiki": { + "name": "Dokuwiki", + "link": "https://www.dokuwiki.org/dokuwiki", + "icon": "https://www.dokuwiki.org/lib/tpl/dokuwiki/images/logo.png", + "desc": "Wissensspeicher" + }, + "owncloud": { + "name": "ownCloud", + "link": "https://owncloud.com/de/infinite-scale/", + "icon": "https://owncloud.com/wp-content/themes/ownCloud/dist/assets/img/favicon/apple-touch-icon.png", + "desc": "Datenablage" + }, + "murmur": { + "name": "Murmur", + "link": "https://www.mumble.info/", + "icon": "https://www.mumble.info/css/mumble.svg", + "desc": "Audiokonferenzen (bislang ohne Authentifizierung)" + } + }, + "order": [ + "synapse", + "element", + "murmur", + "hedgedoc", + "dokuwiki", + "owncloud", + "vikunja", + "gitlab", + "forgejo" + ] + }, + "concrete_planned": { + "pool": { + "bigbluebutton": { + "name": "BigBlueButton", + "link": "https://bigbluebutton.org/", + "icon": null, + "desc": "Videokonferenzen", + "stat": "planned" + }, + "mastodon": { + "name": "Mastodon", + "link": "https://joinmastodon.org", + "icon": "https://creazilla-store.fra1.digitaloceanspaces.com/icons/3204993/logo-mastodon-icon-sm.png", + "desc": "Microblogging", + "stat": "planned" + }, + "grav": { + "name": "grav", + "link": "https://getgrav.org/", + "icon": "https://getgrav.org/user/themes/planetoid/images/favicon.png", + "desc": "Web-Inhaltsverwaltung", + "stat": "planned" + }, + "dovecot": { + "name": "Dovecot", + "link": "https://doc.dovecot.org/", + "icon": "https://w2.influxdata.com/wp-content/uploads/dovecot-logo.png", + "desc": "E-Mail-Empfang", + "stat": "planned" + }, + "postfix": { + "name": "Postfix", + "link": "https://www.postfix.org/", + "icon": "https://webhostinggeeks.com/howto/wp-content/uploads/2012/06/Posfix-Mail.jpg", + "desc": "E-Mail-Versand", + "stat": "planned" + } + }, + "order": [ + "postfix", + "dovecot", + "mastodon", + "grav" + ] + } + }, + "alternatives": { + "list": [ + "MS Exchange", + "Google", + "facebook", + "WhatsApp/Telegram/Signal", + "Dropbox", + "Nextcloud", + "Humhub", + "Zetkin", + "…" + ], + "problems": [ + "zentralisiert", + "proprietär", + "datenschutzverletzend", + "zwielichtig", + "überladen", + "unterladen", + "kompliziert", + "unflexibel", + "kaputt" + ] + } + }, + "user_management": { + "schema": [ + { + "text": "Die Dienste stehen bereit, aber wer darf sie verwenden?", + "image": "user_management-0" + }, + { + "text": "Espe als Nutzerverwaltung …", + "image": "user_management-1" + }, + { + "text": "… hat einen typischen technischen Aufbau", + "image": "user_management-2" + }, + { + "text": "… und kann den Authentifizierungs-Dienst füttern", + "image": "user_management-3" + }, + { + "text": "MGL …", + "image": "user_management-4" + }, + { + "text": "… ist über den Browser bedienbar", + "image": "user_management-5" + }, + { + "text": "… hat anscheinend auch einen typischen technischen Aufbau", + "image": "user_management-6" + }, + { + "text": "… und kann erweitert werden :)", + "image": "user_management-7" + }, + { + "text": "… und damit als Quelle für Espe und den Authentifizierungs-Dienst dienen", + "image": "user_management-8" + } + ], + "processes": { + "entry": [ + { + "text": "Bei Eintritt legt ein Mitgliederbeauftragter einen Datensatz in MGL an", + "image": "user_management-9" + }, + { + "text": "Datensatz landet in MGL-BE/MGL-DB", + "image": "user_management-10" + }, + { + "text": "Syncer liest Mitglieder aus und gleicht in Richtung Espe ab", + "image": "user_management-11" + }, + { + "text": "Espe erzeugt für jeden neuen Nutzerdatensatz ein Passwort und sendet E-Mail", + "image": "user_management-12" + }, + { + "text": "… und schickt die neue Nutzerliste an den Authentifizierungs-Dienst", + "image": "user_management-13" + } + ] + }, + "example": { + "link": "https://zackeneule.linke.sx/", + "label": "zackeneule.linke.sx", + "remark": " (keine MGL-Anbindung)" + } + }, + "realization": { + "todos_technical": [ + { + "name": "Entwickler gewinnen", + "link": null + }, + { + "name": "Penetrationstests durchführen", + "link": "https://de.m.wikipedia.org/wiki/Penetrationstest_(Informatik)" + }, + { + "name": "Lastverteilung vorsehen", + "link": "https://de.m.wikipedia.org/wiki/Lastverteilung_(Informatik)" + }, + { + "name": "Datensicherung einrichten", + "link": "https://de.m.wikipedia.org/wiki/Datensicherung" + }, + { + "name": "Überwachung einrichten", + "link": "https://de.m.wikipedia.org/wiki/Monitoring" + } + ], + "todos_social": [ + { + "name": "Kräfte bündeln", + "link": null + }, + { + "name": "Tester gewinnen", + "link": null + }, + { + "name": "Admins gewinnen", + "link": null + }, + { + "name": "Überzeugungsarbeit leisten", + "link": null + } + ] + }, + "resources": [ + { + "name": "Ansible-Rollen für Dienste", + "link": "https://gitlab.die-linke.cloud/misc/ansible-base" + }, + { + "name": "Infrastruktur-Definition für LAG Netzpolitik Sachsen", + "link": "https://gitlab.die-linke.cloud/misc/infrastructure" + }, + { + "name": "Espe | Datenmodell", + "link": "https://gitlab.die-linke.cloud/espe/datamodel" + }, + { + "name": "Espe | Backend", + "link": "https://gitlab.die-linke.cloud/espe/backend" + }, + { + "name": "Espe | Frontend | Web", + "link": "https://gitlab.die-linke.cloud/espe/frontend-zackeneule" + }, + { + "name": "Espe | Frontend | CLI", + "link": "https://gitlab.die-linke.cloud/espe/frontend-mondvogel" + }, + { + "name": "Espe | Ansible-Rollen", + "link": "https://gitlab.die-linke.cloud/espe/infrastructure" + }, + { + "name": "MGL-CLI-Client", + "link": "https://gitlab.die-linke.cloud/misc/mgl-cli" + }, + { + "name": "MGL-Espe-Syncer", + "link": "https://gitlab.die-linke.cloud/misc/mgl-espe-syncer" + } + ] +} + diff --git a/source/graphs b/source/graphs new file mode 100755 index 0000000..bc137f4 --- /dev/null +++ b/source/graphs @@ -0,0 +1,619 @@ +#!/usr/bin/env python3 + +import os as _os +import sys as _sys +import json as _json +import argparse as _argparse + +from lib import * + + +def hue_regular(): + return 0.4 + + +def hue_highlight(): + return 0.0 + + +def style(level, step): + return ("invis" if (step < level) else "filled") + + +def style_new(predicate): + return ( + "filled" + if predicate() else + "invis" + ) + + +def highlight(predicate): + return ( + { + "fillcolor": ("%.4f+0.75+0.5" % (hue_highlight(), )), + "color": ("%.4f+0.75+0.5" % (hue_highlight(), )), + } + if predicate() else + { + } + ) + + +def render(input_, data, format_, path_output): + path_temp = "/tmp/graph.gv" + _os.makedirs(_os.path.dirname(path_temp), exist_ok = True) + file_write( + path_temp, + string_coin( + input_, + data + ) + ) + _os.makedirs(_os.path.dirname(path_output), exist_ok = True) + _os.system( + string_coin( + "cat {{path_source}} | dot -T {{format}} > {{path_output}}", + { + "path_source": path_temp, + "path_output": path_output, + "format": format_, + } + ) + ) + + +def attributes_subgraph(): + return { + "fontname": "monospace", + "fontcolor": ("%.4f+0+1" % (hue_regular(), )), + "color": ("%.4f+0.25+0.25" % (hue_regular(), )), + "fillcolor": ("%.4f+0.25+0.25" % (hue_regular(), )), + } + + +def dot_macro_services(conf): + return dot_graph( + { + "name": "services", + "settings": { + "graph": { + "layout": "dot", + "rankdir": "BT", + "bgcolor": "0.4+0+0.125", + }, + "node": { + "fontname": "monospace", + "shape": "hexagon", + "style": "filled", + "fillcolor": "0.4+0.75+0.5", + "color": "0.4+0.75+0.5", + "fontcolor": "0.4+0+1", + }, + "edge": { + "fontname": "monospace", + "color": "0.4+0+0.75", + } + }, + "nodes": [ + { + "name": "node_db", + "attributes": { + "label": "DB-Server", + "style": style_new(lambda: (conf["step"] >= 1)), + }, + }, + { + "name": "node_web", + "attributes": { + "label": "Web-Server", + "style": style_new(lambda: (conf["step"] >= 2)), + }, + }, + { + "name": "node_auth", + "attributes": { + "label": "Auth-Server", + "style": style_new(lambda: (conf["step"] >= 3)), + }, + }, + { + "name": "node_db_for_auth", + "attributes": { + "label": "DB-Modul\nfür\nAuth-Server", + "style": style_new(lambda: (conf["step"] >= 4)), + }, + }, + { + "name": "node_auth_and_web", + "attributes": { + "label": "Web-Modul\nfür\nAuth-Server", + "style": style_new(lambda: (conf["step"] >= 5)), + }, + }, + { + "name": "node_service", + "attributes": { + "label": "Dienst", + "style": style_new(lambda: (conf["step"] >= 6)), + }, + }, + { + "name": "node_db_for_service", + "attributes": { + "label": "DB-Modul\nfür\nDienst", + "style": style_new(lambda: (conf["step"] >= 7)), + }, + }, + { + "name": "node_auth_for_service", + "attributes": { + "label": "Auth-Modul\nfür\nDienst", + "style": style_new(lambda: (conf["step"] >= 8)), + }, + }, + { + "name": "node_service_and_web", + "attributes": { + "label": "Web-Modul\nfür\nDienst", + "style": style_new(lambda: (conf["step"] >= 9)), + }, + }, + ], + "edges": [ + { + "name_from": "node_db", + "name_to": "node_db_for_auth", + "attributes": { + "style": style_new(lambda: (conf["step"] >= 4)), + }, + }, + { + "name_from": "node_db_for_auth", + "name_to": "node_auth", + "attributes": { + "style": style_new(lambda: (conf["step"] >= 4)), + }, + }, + + + { + "name_from": "node_auth", + "name_to": "node_auth_and_web", + "attributes": { + "style": style_new(lambda: (conf["step"] >= 5)), + }, + }, + { + "name_from": "node_web", + "name_to": "node_auth_and_web", + "attributes": { + "style": style_new(lambda: (conf["step"] >= 5)), + }, + }, + { + "name_from": "node_db", + "name_to": "node_db_for_service", + "attributes": { + "style": style_new(lambda: (conf["step"] >= 7)), + }, + }, + { + "name_from": "node_db_for_service", + "name_to": "node_service", + "attributes": { + "style": style_new(lambda: (conf["step"] >= 7)), + }, + }, + { + "name_from": "node_auth", + "name_to": "node_auth_for_service", + "attributes": { + "style": style_new(lambda: (conf["step"] >= 8)), + }, + }, + { + "name_from": "node_auth_for_service", + "name_to": "node_service", + "attributes": { + "style": style_new(lambda: (conf["step"] >= 8)), + }, + }, + { + "name_from": "node_service", + "name_to": "node_service_and_web", + "attributes": { + "style": style_new(lambda: (conf["step"] >= 9)), + }, + }, + { + "name_from": "node_web", + "name_to": "node_service_and_web", + "attributes": { + "style": style_new(lambda: (conf["step"] >= 9)), + }, + }, + ], + "subgraphs": [ + ], + } + ) + + +def dot_macro_user_management(conf): + return dot_graph( + { + "name": "user_management", + "settings": { + "graph": { + "layout": "dot", + "rankdir": "BT", + "bgcolor": "0.4+0+0.125", + }, + "node": { + "fontname": "monospace", + "shape": "hexagon", + "style": "filled", + "fillcolor": "0.4+0.75+0.5", + "color": "0.4+0.75+0.5", + "fontcolor": "0.4+0+1", + }, + "edge": { + "fontname": "monospace", + "color": "0.4+0+0.75", + } + }, + "nodes": [ + { + "name": "node_9", + "attributes": ( + { + "label": "Syncer", + "style": style_new(lambda: (conf["step"] >= 8)), + } + | + highlight(lambda: (conf["step"] == 11)) + ) + }, + ], + "edges": [ + { + "name_from": "node_6", + "name_to": "node_11", + "attributes": { + "style": style_new(lambda: (conf["step"] >= 3)), + } + }, + { + "name_from": "node_4", + "name_to": "node_9", + "attributes": { + "dir": "forward", + "style": style_new(lambda: (conf["step"] >= 8)), + } + }, + { + "name_from": "node_8", + "name_to": "node_9", + "attributes": { + "dir": "both", + "style": style_new(lambda: (conf["step"] >= 8)), + } + }, + ], + "subgraphs": [ + { + "name": "3", + "attributes": ( + attributes_subgraph() + | + { + "label": "MGL", + "style": style_new(lambda: (conf["step"] >= 4)), + } + ), + "nodes": [ + { + "name": "node_1", + "attributes": ( + { + "label": "MGL-DB", + "style": style_new(lambda: (conf["step"] >= 6)), + } + | + highlight(lambda: (conf["step"] == 10)) + ) + }, + { + "name": "node_2", + "attributes": ( + { + "label": "MGL-BE", + "style": style_new(lambda: (conf["step"] >= 6)), + } + | + highlight(lambda: (conf["step"] == 10)) + ) + }, + { + "name": "node_3", + "attributes": ( + { + "label": "MGL-FE-Web", + "style": style_new(lambda: (conf["step"] >= 5)), + } + | + highlight(lambda: (conf["step"] == 9)) + ) + }, + { + "name": "node_4", + "attributes": ( + { + "label": "MGL-FE-CLI", + "style": style_new(lambda: (conf["step"] >= 7)), + } + | + highlight(lambda: (conf["step"] == 11)) + ) + }, + ], + "edges": [ + { + "name_from": "node_1", + "name_to": "node_2", + "attributes": { + "dir": "both", + "style": style_new(lambda: (conf["step"] >= 6)), + } + }, + { + "name_from": "node_2", + "name_to": "node_3", + "attributes": { + "dir": "both", + "style": style_new(lambda: (conf["step"] >= 6)), + } + }, + { + "name_from": "node_2", + "name_to": "node_4", + "attributes": { + "dir": "forward", + "style": style_new(lambda: (conf["step"] >= 7)), + } + }, + ], + }, + { + "name": "2", + "attributes": ( + attributes_subgraph() + | + { + "label": "Espe", + "style": style_new(lambda: (conf["step"] >= 1)), + } + ), + "nodes": [ + { + "name": "node_5", + "attributes": ( + { + "label": "Espe-DB", + "style": style_new(lambda: (conf["step"] >= 2)), + } + | + highlight(lambda: (conf["step"] == 12)) + ) + }, + { + "name": "node_6", + "attributes": ( + { + "label": "Espe-BE", + "style": style_new(lambda: (conf["step"] >= 2)), + } + | + highlight(lambda: (conf["step"] == 12)) + ) + }, + { + "name": "node_8", + "attributes": ( + { + "label": "Espe-FE-CLI", + "style": style_new(lambda: (conf["step"] >= 2)), + } + | + highlight(lambda: (conf["step"] == 11)) + ) + }, + { + "name": "node_7", + "attributes": ( + { + "label": "Espe-FE-Web", + "style": style_new(lambda: (conf["step"] >= 2)), + } + ) + }, + ], + "edges": [ + { + "name_from": "node_5", + "name_to": "node_6", + "attributes": { + "dir": "both", + "style": style_new(lambda: (conf["step"] >= 2)), + } + }, + { + "name_from": "node_6", + "name_to": "node_7", + "attributes": { + "dir": "both", + "style": style_new(lambda: (conf["step"] >= 2)), + } + }, + { + "name_from": "node_6", + "name_to": "node_8", + "attributes": { + "dir": "both", + "style": style_new(lambda: (conf["step"] >= 2)), + } + }, + ], + }, + { + "name": "1", + "attributes": ( + attributes_subgraph() + | + { + "label": "Dienste", + "style": style_new(lambda: (conf["step"] >= 0)), + } + ), + "nodes": [ + { + "name": "node_10", + "attributes": { + "label": "…", + "style": style_new(lambda: (conf["step"] >= 0)), + } + }, + { + "name": "node_11", + "attributes": ( + { + "label": "Auth-Server", + "style": style_new(lambda: (conf["step"] >= 0)), + } + | + highlight(lambda: (conf["step"] == 13)) + ) + }, + { + "name": "node_12", + "attributes": { + "label": "…", + "style": style_new(lambda: (conf["step"] >= 0)), + } + }, + ], + "edges": [ + { + "name_from": "node_10", + "name_to": "node_11", + "attributes": { + "dir": "both", + "style": style_new(lambda: (conf["step"] >= 0)), + } + }, + { + "name_from": "node_11", + "name_to": "node_12", + "attributes": { + "dir": "both", + "style": style_new(lambda: (conf["step"] >= 0)), + } + }, + ], + }, + ] + } + ) + + +def main(): + ## consts + dir_source = "source" + + ## args + argument_parser = _argparse.ArgumentParser( + ) + argument_parser.add_argument( + "-f", + "--format", + type = str, + choices = ["svg","png"], + dest = "format", + metavar = "", + default = "svg" + ) + argument_parser.add_argument( + "-n", + "--no-extension", + action = "store_true", + dest = "no_extension", + default = False, + ) + argument_parser.add_argument( + "-o", + "--output-directory", + type = str, + dest = "output_directory", + metavar = "", + default = "temp" + ) + args = argument_parser.parse_args() + + ## exec + ### excc:services + if True: + size = 10 + for step in range(size): + render( + dot_macro_services( + { + "step": step, + } + ), + {}, + args.format, + string_coin( + "{{directory}}/{{name}}-{{step}}{{extension}}", + { + "name": "services", + "directory": args.output_directory, + "step": ("%u" % step), + "extension": ( + "" + if args.no_extension else + ("." + args.format) + ), + } + ) + ) + ### exec:syncing + if True: + size = 14 + for step in range(size): + render( + dot_macro_user_management( + { + "step": step, + } + ), + {}, + args.format, + string_coin( + "{{directory}}/{{name}}-{{step}}{{extension}}", + { + "name": "user_management", + "directory": args.output_directory, + "step": ("%u" % step), + "extension": ( + "" + if args.no_extension else + ("." + args.format) + ), + } + ) + ) + + +main() + diff --git a/source/lib.py b/source/lib.py new file mode 100644 index 0000000..32e2fb0 --- /dev/null +++ b/source/lib.py @@ -0,0 +1,199 @@ +import os as _os + + +def convey(x, fs): + y = x + for f in fs: + y = f(y) + return y + + +def string_coin(template, arguments, options = None): + options = ( + { + "open": "{{", + "close": "}}", + } + | + (options or {}) + ) + result = template + for (key, value, ) in arguments.items(): + if (value is None): + pass + else: + result = result.replace( + ( + "%s%s%s" + % ( + options["open"], + key, + options["close"], + ) + ), + value + ) + return result + + +def file_read(path): + handle = open(path, "r") + content = handle.read() + handle.close() + return content + + +def file_write(path, content): + _os.makedirs(_os.path.dirname(path), exist_ok = True) + handle = open(path, "w" if _os.path.exists(path) else "a") + handle.write(content) + handle.close() + return None + + +def dot_entry(head, attributes, options = None): + options = ( + { + "indentation": 0, + } + | + (options or {}) + ) + return string_coin( + "{{indentation}}{{head}} [{{attributes}}];\n", + { + "indentation": ("\t" * options["indentation"]), + "head": head, + "attributes": convey( + attributes, + [ + lambda x: x.items(), + lambda x: map( + lambda pair: string_coin( + "{{key}}=\"{{value}}\"", + { + "key": pair[0], + "value": pair[1] + } + ), + x + ), + ", ".join, + ] + ), + } + ) + + +def dot_node(node, options = None): + options = ( + { + "indentation": 0, + } + | + (options or {}) + ) + return dot_entry( + string_coin("{{name}}", {"name": node["name"]}), + node["attributes"], + options + ) + + +def dot_edge(edge, options = None): + options = ( + { + "indentation": 0, + } + | + (options or {}) + ) + return dot_entry( + string_coin("{{name_from}} -> {{name_to}}", {"name_from": edge["name_from"], "name_to": edge["name_to"]}), + edge["attributes"], + options + ) + + +def dot_subgraph(subgraph, options = None): + options = ( + { + "indentation": 0, + } + | + (options or {}) + ) + return string_coin( + ( + "{{indentation}}subgraph cluster_{{name}}\n" + + + "{{indentation}}{\n" + + + "{{attributes}}\n" + + + "{{nodes}}\n" + + + "{{edges}}\n" + + + "{{indentation}}}\n" + ), + { + "indentation": ("\t" * options["indentation"]), + "name": subgraph["name"], + "attributes": convey( + subgraph["attributes"], + [ + lambda x: x.items(), + lambda x: map( + lambda pair: string_coin( + "{{indentation}}{{key}}=\"{{value}}\";\n", + { + "indentation": ("\t" * (options["indentation"] + 1)), + "key": pair[0], + "value": pair[1] + } + ), + x + ), + "".join, + ] + ), + "nodes": "".join(map(lambda node: dot_node(node, {"indentation": (options["indentation"] + 1)}), subgraph["nodes"])), + "edges": "".join(map(lambda edge: dot_edge(edge, {"indentation": (options["indentation"] + 1)}), subgraph["edges"])), + } + ) + + +def dot_graph(graph): + return string_coin( + ( + "digraph {{name}}\n" + + + "{\n" + + + "{{settings_graph}}\n" + + + "{{settings_node}}\n" + + + "{{settings_edge}}\n" + + + "{{subgraphs}}\n" + + + "{{nodes}}\n" + + + "{{edges}}\n" + + + "}\n" + ), + { + "name": graph["name"], + "settings_graph": dot_entry("graph", graph["settings"]["graph"], {"indentation": 1}), + "settings_node": dot_entry("node", graph["settings"]["node"], {"indentation": 1}), + "settings_edge": dot_entry("edge", graph["settings"]["edge"], {"indentation": 1}), + "nodes": "".join(map(lambda node: dot_node(node, {"indentation": 1}), graph["nodes"])), + "edges": "".join(map(lambda edge: dot_edge(edge, {"indentation": 1}), graph["edges"])), + "subgraphs": "".join(map(lambda subgraph: dot_subgraph(subgraph, {"indentation": 1}), graph["subgraphs"])), + } + ) + + diff --git a/source/media/icons/ansible.png b/source/media/icons/ansible.png new file mode 100644 index 0000000..62cb72c Binary files /dev/null and b/source/media/icons/ansible.png differ diff --git a/source/media/icons/authelia.png b/source/media/icons/authelia.png new file mode 100644 index 0000000..3c6f78e Binary files /dev/null and b/source/media/icons/authelia.png differ diff --git a/source/media/icons/debian.png b/source/media/icons/debian.png new file mode 100644 index 0000000..42868fc Binary files /dev/null and b/source/media/icons/debian.png differ diff --git a/source/media/icons/dokuwiki.png b/source/media/icons/dokuwiki.png new file mode 100644 index 0000000..efadf8c Binary files /dev/null and b/source/media/icons/dokuwiki.png differ diff --git a/source/media/icons/dovecot.png b/source/media/icons/dovecot.png new file mode 100644 index 0000000..1ce188c Binary files /dev/null and b/source/media/icons/dovecot.png differ diff --git a/source/media/icons/element.png b/source/media/icons/element.png new file mode 100644 index 0000000..1052f04 Binary files /dev/null and b/source/media/icons/element.png differ diff --git a/source/media/icons/forgejo.png b/source/media/icons/forgejo.png new file mode 100644 index 0000000..1e6aa45 Binary files /dev/null and b/source/media/icons/forgejo.png differ diff --git a/source/media/icons/gitlab.png b/source/media/icons/gitlab.png new file mode 100644 index 0000000..0e4397c Binary files /dev/null and b/source/media/icons/gitlab.png differ diff --git a/source/media/icons/grav.png b/source/media/icons/grav.png new file mode 100644 index 0000000..e7d6435 Binary files /dev/null and b/source/media/icons/grav.png differ diff --git a/source/media/icons/hedgedoc.png b/source/media/icons/hedgedoc.png new file mode 100644 index 0000000..9dedc95 Binary files /dev/null and b/source/media/icons/hedgedoc.png differ diff --git a/source/media/icons/inwx.png b/source/media/icons/inwx.png new file mode 100644 index 0000000..664bc35 Binary files /dev/null and b/source/media/icons/inwx.png differ diff --git a/source/media/icons/keepassxc.png b/source/media/icons/keepassxc.png new file mode 100644 index 0000000..771579b Binary files /dev/null and b/source/media/icons/keepassxc.png differ diff --git a/source/media/icons/letsencrypt.png b/source/media/icons/letsencrypt.png new file mode 100644 index 0000000..96ccb7e Binary files /dev/null and b/source/media/icons/letsencrypt.png differ diff --git a/source/media/icons/mastodon.png b/source/media/icons/mastodon.png new file mode 100644 index 0000000..9ed185f Binary files /dev/null and b/source/media/icons/mastodon.png differ diff --git a/source/media/icons/murmur.png b/source/media/icons/murmur.png new file mode 100644 index 0000000..bdbd47b Binary files /dev/null and b/source/media/icons/murmur.png differ diff --git a/source/media/icons/nginx.png b/source/media/icons/nginx.png new file mode 100644 index 0000000..38376b4 Binary files /dev/null and b/source/media/icons/nginx.png differ diff --git a/source/media/icons/owncloud.png b/source/media/icons/owncloud.png new file mode 100644 index 0000000..61a987c Binary files /dev/null and b/source/media/icons/owncloud.png differ diff --git a/source/media/icons/postfix.png b/source/media/icons/postfix.png new file mode 100644 index 0000000..eeafb8e Binary files /dev/null and b/source/media/icons/postfix.png differ diff --git a/source/media/icons/postgresql.png b/source/media/icons/postgresql.png new file mode 100644 index 0000000..f9aa635 Binary files /dev/null and b/source/media/icons/postgresql.png differ diff --git a/source/media/icons/synapse.png b/source/media/icons/synapse.png new file mode 100644 index 0000000..91a0383 Binary files /dev/null and b/source/media/icons/synapse.png differ diff --git a/source/media/icons/vikunja.png b/source/media/icons/vikunja.png new file mode 100644 index 0000000..ae0c623 Binary files /dev/null and b/source/media/icons/vikunja.png differ diff --git a/source/tex/app.tex.tpl b/source/tex/app.tex.tpl new file mode 100644 index 0000000..e58afad --- /dev/null +++ b/source/tex/app.tex.tpl @@ -0,0 +1,9 @@ + \begin{item} + \href{<>} + { + \includegraphics[width=0.025\textwidth]{media/icons/<>} + \ + \textbf{<>} + } — <> + \end{item} + diff --git a/source/tex/master.tex.tpl b/source/tex/master.tex.tpl new file mode 100644 index 0000000..bf97d19 --- /dev/null +++ b/source/tex/master.tex.tpl @@ -0,0 +1,190 @@ +\documentclass[aspectratio=169]{beamer} + +\usepackage{graphicx} +\usepackage{svg} +% \usepackage[ngerman]{babel} + +\date{<>} +\author{<>} +\title{<>} + +% \setmainlanguage{german} + +\usetheme{Luebeck} +% \usetheme{m} +\usecolortheme{spruce} + + +\begin{document} + + \maketitle + + \begin{frame} + \tableofcontents + \end{frame} + + \begin{section}{Dienste} + + \begin{frame} + \sectionpage + \end{frame} + + \begin{subsection}{Theorie} + +<> + + \end{subsection} + + \begin{subsection}{Praxis} + + \begin{frame} + \frametitle{Beispiel} + + Siehe \href{<>}{\texttt{<>}} + \end{frame} + + \begin{frame} + \frametitle{Grundlegende Dientse} + + \begin{itemize} +<> + \end{itemize} + \end{frame} + + \begin{frame} + \frametitle{Konkrete Dienste} + + \begin{itemize} +<> + \end{itemize} + \end{frame} + + \begin{frame} + \frametitle{Geplante Dienste} + + \begin{itemize} +<> + \end{itemize} + \end{frame} + + \begin{frame} + \frametitle{Eingesetzte Technik} + + \begin{itemize} +<> + \end{itemize} + \end{frame} + + \end{subsection} + + \begin{subsection}{Alternativen} + + \begin{frame} + \frametitle{„Nimm doch einfach …“} + + \begin{columns} + \pause + \begin{column}{0.5\textwidth} + \begin{itemize} +<> + \end{itemize} + \end{column} + \pause + \begin{column}{0.5\textwidth} + \begin{itemize} +<> + \end{itemize} + \end{column} + \end{columns} + + \end{frame} + + \begin{frame} + \frametitle{„Aber es gibt doch noch …“} + + \pause + Du kennst ein System, was alles Gewünschte halbwegs sauber, sicher und bezahlbar abbildet? \\ + \pause + \vspace{10mm} + Na geil! Dann schick mir gerne eine Nachricht: \texttt{christian.frass@dielinke-glauchau.de} \\ + \pause + \vspace{10mm} + … aber rechne besser damit, dass ich es rösten oder ganz in der Luft zerreißen werde :) + \end{frame} + + \end{subsection} + + \end{section} + + \begin{section}{Nutzerverwaltung} + + \begin{frame} + \sectionpage + \end{frame} + + \begin{subsection}{Aufbau} +<> + \end{subsection} + + \begin{subsection}{Ablauf} + +<> + + \end{subsection} + + \begin{frame} + \frametitle{Vorführung} + + Siehe \href{<>}{\texttt{<>}} <> + \end{frame} + + \end{section} + + \begin{section}{Umsetzung} + + \begin{frame} + \frametitle{Zu tun} + + \begin{columns} + \pause + \begin{column}{0.5\textwidth} + \textbf{technisch} + \begin{itemize} +<> + \end{itemize} + \end{column} + \pause + \begin{column}{0.5\textwidth} + \textbf{menschlich} + \begin{itemize} +<> + \end{itemize} + \end{column} + \end{columns} + \end{frame} + +<> + + \end{section} + + \begin{section}{Schluss} + \begin{frame} + \frametitle{Lust bekommen?} + + \begin{itemize} + \item{E-Mail an \href{mailto:<>}{\texttt{<>}}} + \pause + \item{Teilnahme in \texttt{\$\{gemeinsamer-kommunikations-kanal\}}?} + \end{itemize} + \end{frame} + + \begin{frame} + \begin{center} + 's'war's :) + \end{center} + \end{frame} + + \end{section} + +\end{document} + diff --git a/source/tex/resource-item.tex.tpl b/source/tex/resource-item.tex.tpl new file mode 100644 index 0000000..4bdb615 --- /dev/null +++ b/source/tex/resource-item.tex.tpl @@ -0,0 +1,6 @@ + \begin{item} + \href{<>} + { + <> + } + \end{item} diff --git a/source/tex/resources-frame.tex.tpl b/source/tex/resources-frame.tex.tpl new file mode 100644 index 0000000..672a6ac --- /dev/null +++ b/source/tex/resources-frame.tex.tpl @@ -0,0 +1,8 @@ + \begin{frame} + \frametitle{Ressourcen} + + \begin{itemize} +<> + \end{itemize} + \end{frame} + diff --git a/source/tex/service-theory-frame.tex.tpl b/source/tex/service-theory-frame.tex.tpl new file mode 100644 index 0000000..f38777d --- /dev/null +++ b/source/tex/service-theory-frame.tex.tpl @@ -0,0 +1,16 @@ + \begin{frame} + \frametitle{Theorie} + \begin{columns} + % \begin{column}{0.618\textwidth} + \begin{column}{0.5\textwidth} + \begin{center} + \includesvg[inkscapelatex=false,height=0.8\textheight]{graphs/<>} + \end{center} + \end{column} + % \begin{column}{0.382\textwidth} + \begin{column}{0.5\textwidth} + <> + \end{column} + \end{columns} + \end{frame} + diff --git a/source/tex/todo.tex.tpl b/source/tex/todo.tex.tpl new file mode 100644 index 0000000..4a51253 --- /dev/null +++ b/source/tex/todo.tex.tpl @@ -0,0 +1,4 @@ + \begin{item} + \href{<>}{<>} + \end{item} + diff --git a/source/tex/user_management-frame.tex.tpl b/source/tex/user_management-frame.tex.tpl new file mode 100644 index 0000000..ff697fb --- /dev/null +++ b/source/tex/user_management-frame.tex.tpl @@ -0,0 +1,8 @@ + \begin{frame} + \frametitle{<>} + \begin{figure} + \includesvg[inkscapelatex=false,width=1.0\textwidth]{graphs/<<image>>} + \end{figure} + <<text>> + \end{frame} + diff --git a/tools/build b/tools/build new file mode 100755 index 0000000..34d89fe --- /dev/null +++ b/tools/build @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 + +import os as _os +import argparse as _argparse + + +def main(): + ## args + argument_parser = _argparse.ArgumentParser( + ) + argument_parser.add_argument( + "-c", + "--clear", + action = "store_true", + dest = "clear", + default = False + ) + argument_parser.add_argument( + "-r", + "--include-resources", + action = "store_true", + dest = "include_resources", + default = False, + ) + argument_parser.add_argument( + "-v", + "--verbose", + action = "store_true", + dest = "verbose", + default = False, + ) + argument_parser.add_argument( + "-o", + "--output-directory", + type = str, + dest = "output_directory", + metavar = "<output-directory>", + default = "build" + ) + args = argument_parser.parse_args() + + ## exec + make_args = [] + make_args.append("dir_build=%s" % args.output_directory) + if args.include_resources: + make_args.append("coin_args='-r'") + if not args.verbose: + make_args.append("latex_args='-interaction batchmode'") + if args.clear: + make_args.append("clear") + make_args.append("all") + _os.system("make -f tools/makefile %s" % " ".join(make_args)) + + +main() + diff --git a/tools/coin b/tools/coin new file mode 100755 index 0000000..e4f62f4 --- /dev/null +++ b/tools/coin @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 + +import sys as _sys +import json as _json +import argparse as _argparse + +from lib import * + + +_template_cache = {} + + +def coin(template, arguments): + return string_coin( + template, + arguments, + {"open": "<<", "close": ">>"} + ) + + +def render(template_name, arguments): + global _template_cache + if (not (template_name in _template_cache)): + template_content = file_read("source/tex/%s.tex.tpl" % template_name) + _template_cache[template_name] = template_content + else: + template_content = _template_cache[template_name] + return coin( + template_content, + arguments + ) + + +def macro_applist(node): + return convey( + node["order"], + [ + lambda x: map( + lambda y: render( + "app", + { + "id": y, + "name": node["pool"][y]["name"], + "link": node["pool"][y]["link"], + "icon": node["pool"][y]["icon"], + "desc": node["pool"][y]["desc"], + } + ), + x + ), + "".join, + ] + ) + + +def macro_todolist(node): + return convey( + node, + [ + lambda x: map( + lambda entry: render( + "todo", + { + "name": entry["name"], + "link": entry["link"], + } + ), + x + ), + "".join, + ] + ) + + +def main(): + ## args + argument_parser = _argparse.ArgumentParser() + argument_parser.add_argument( + "-d", + "--data-path", + type = str, + dest = "data_path", + metavar = "<data-path>", + default = "source/data.json", + ) + argument_parser.add_argument( + "-r", + "--include-resources", + action = "store_true", + dest = "include_resources", + default = False, + ) + args = argument_parser.parse_args() + + ## exec + data = _json.loads(file_read(args.data_path)) + _sys.stdout.write( + render( + "master", + { + "meta-date": data["meta"]["date"], + "meta-author": data["meta"]["author"], + "meta-title": data["meta"]["title"], + "meta-contact-email_address": data["meta"]["contact"]["email_address"], + "service-theory-frames": convey( + data["services"]["theory"], + [ + lambda x: map( + lambda y: render( + "service-theory-frame", + y + ), + x + ), + "".join, + ] + ), + "service-practice-example-link": data["services"]["practice"]["example"]["link"], + "service-practice-example-label": data["services"]["practice"]["example"]["label"], + "service-practice-technologies-items": macro_applist(data["services"]["practice"]["technologies"]), + "service-practice-basics-items": macro_applist(data["services"]["practice"]["basics"]), + "service-practice-existing-items": macro_applist(data["services"]["practice"]["concrete_present"]), + "service-practice-future-items": macro_applist(data["services"]["practice"]["concrete_planned"]), + "services-alternatives-list-items": convey( + data["services"]["alternatives"]["list"], + [ + lambda x: map( + lambda entry: coin("\\item{<<entry>>}\n", {"entry": entry}), + x + ), + "".join, + ] + ), + "services-alternatives-problems-items": convey( + data["services"]["alternatives"]["problems"], + [ + lambda x: map( + lambda entry: coin("\\item{<<entry>>}\n", {"entry": entry}), + x + ), + "".join, + ] + ), + "user_management-schema-frames": convey( + data["user_management"]["schema"], + [ + lambda x: map( + lambda y: render( + "user_management-frame", + ({"title": "Aufbau"} | y) + ), + x + ), + "".join, + ] + ), + "user_management-process-entry-frames": convey( + data["user_management"]["processes"]["entry"], + [ + lambda x: map( + lambda y: render( + "user_management-frame", + ({"title": "Ablauf"} | y) + ), + x + ), + "".join, + ] + ), + "user_management-example-link": data["user_management"]["example"]["link"], + "user_management-example-label": data["user_management"]["example"]["label"], + "user_management-example-remark": data["user_management"]["example"]["remark"], + "resources-frame": ( + render( + "resources-frame", + { + "resource-items": convey( + data["resources"], + [ + lambda x: map( + lambda resource: render( + "resource-item", + { + "name": resource["name"], + "link": resource["link"], + } + ), + x + ), + "".join, + ] + ), + } + ) + if args.include_resources else + "" + ), + "todos-technical-items": macro_todolist(data["realization"]["todos_technical"]), + "todos-social-items": macro_todolist(data["realization"]["todos_social"]), + } + ) + ) + + +main() + diff --git a/tools/fetch b/tools/fetch new file mode 100755 index 0000000..483881b --- /dev/null +++ b/tools/fetch @@ -0,0 +1,8 @@ +#!/usr/bin/env python3 + +import json as _json + +data = _json.loads(open("source/data.json").read()) +for pair in data["service"]["practice"]["pool"].items(): + print("# %s\ncurl -L %s -o source/media/icons/%s\n" % (pair[0], pair[1]["icon"], pair[0])) + diff --git a/tools/lib.py b/tools/lib.py new file mode 100644 index 0000000..32e2fb0 --- /dev/null +++ b/tools/lib.py @@ -0,0 +1,199 @@ +import os as _os + + +def convey(x, fs): + y = x + for f in fs: + y = f(y) + return y + + +def string_coin(template, arguments, options = None): + options = ( + { + "open": "{{", + "close": "}}", + } + | + (options or {}) + ) + result = template + for (key, value, ) in arguments.items(): + if (value is None): + pass + else: + result = result.replace( + ( + "%s%s%s" + % ( + options["open"], + key, + options["close"], + ) + ), + value + ) + return result + + +def file_read(path): + handle = open(path, "r") + content = handle.read() + handle.close() + return content + + +def file_write(path, content): + _os.makedirs(_os.path.dirname(path), exist_ok = True) + handle = open(path, "w" if _os.path.exists(path) else "a") + handle.write(content) + handle.close() + return None + + +def dot_entry(head, attributes, options = None): + options = ( + { + "indentation": 0, + } + | + (options or {}) + ) + return string_coin( + "{{indentation}}{{head}} [{{attributes}}];\n", + { + "indentation": ("\t" * options["indentation"]), + "head": head, + "attributes": convey( + attributes, + [ + lambda x: x.items(), + lambda x: map( + lambda pair: string_coin( + "{{key}}=\"{{value}}\"", + { + "key": pair[0], + "value": pair[1] + } + ), + x + ), + ", ".join, + ] + ), + } + ) + + +def dot_node(node, options = None): + options = ( + { + "indentation": 0, + } + | + (options or {}) + ) + return dot_entry( + string_coin("{{name}}", {"name": node["name"]}), + node["attributes"], + options + ) + + +def dot_edge(edge, options = None): + options = ( + { + "indentation": 0, + } + | + (options or {}) + ) + return dot_entry( + string_coin("{{name_from}} -> {{name_to}}", {"name_from": edge["name_from"], "name_to": edge["name_to"]}), + edge["attributes"], + options + ) + + +def dot_subgraph(subgraph, options = None): + options = ( + { + "indentation": 0, + } + | + (options or {}) + ) + return string_coin( + ( + "{{indentation}}subgraph cluster_{{name}}\n" + + + "{{indentation}}{\n" + + + "{{attributes}}\n" + + + "{{nodes}}\n" + + + "{{edges}}\n" + + + "{{indentation}}}\n" + ), + { + "indentation": ("\t" * options["indentation"]), + "name": subgraph["name"], + "attributes": convey( + subgraph["attributes"], + [ + lambda x: x.items(), + lambda x: map( + lambda pair: string_coin( + "{{indentation}}{{key}}=\"{{value}}\";\n", + { + "indentation": ("\t" * (options["indentation"] + 1)), + "key": pair[0], + "value": pair[1] + } + ), + x + ), + "".join, + ] + ), + "nodes": "".join(map(lambda node: dot_node(node, {"indentation": (options["indentation"] + 1)}), subgraph["nodes"])), + "edges": "".join(map(lambda edge: dot_edge(edge, {"indentation": (options["indentation"] + 1)}), subgraph["edges"])), + } + ) + + +def dot_graph(graph): + return string_coin( + ( + "digraph {{name}}\n" + + + "{\n" + + + "{{settings_graph}}\n" + + + "{{settings_node}}\n" + + + "{{settings_edge}}\n" + + + "{{subgraphs}}\n" + + + "{{nodes}}\n" + + + "{{edges}}\n" + + + "}\n" + ), + { + "name": graph["name"], + "settings_graph": dot_entry("graph", graph["settings"]["graph"], {"indentation": 1}), + "settings_node": dot_entry("node", graph["settings"]["node"], {"indentation": 1}), + "settings_edge": dot_entry("edge", graph["settings"]["edge"], {"indentation": 1}), + "nodes": "".join(map(lambda node: dot_node(node, {"indentation": 1}), graph["nodes"])), + "edges": "".join(map(lambda edge: dot_edge(edge, {"indentation": 1}), graph["edges"])), + "subgraphs": "".join(map(lambda subgraph: dot_subgraph(subgraph, {"indentation": 1}), graph["subgraphs"])), + } + ) + + diff --git a/tools/makefile b/tools/makefile new file mode 100644 index 0000000..4b24cab --- /dev/null +++ b/tools/makefile @@ -0,0 +1,61 @@ +## consts/vars + +dir_source := source +dir_temp := temp +dir_build := build +coin_args := +latex_args := + + +## commands + +cmd_log := echo "--" +cmd_rm := rm --recursive --force +cmd_mkdir := mkdir --parents +cmd_cp := cp --recursive --update --verbose +cmd_latex := xelatex -shell-escape ${latex_args} + + +## rules + +.PHONY: _default +_default: all + +.PHONY: all +all: icons graphs ${dir_build}/infra.pdf + +.PHONY: clear +clear: + @ ${cmd_log} "clearing …" + @ ${cmd_rm} ${dir_temp} ${dir_build} + +.PHONY: graphs +graphs: ${dir_source}/graphs + @ ${cmd_log} "graphs …" + @ ${cmd_mkdir} ${dir_temp}/graphs + @ ${dir_source}/graphs --output-directory=${dir_temp}/graphs --format=svg # --no-extension + +.PHONY: icons +icons: + @ ${cmd_log} "icons …" + @ ${cmd_mkdir} ${dir_temp}/media/icons + @ ${cmd_cp} ${dir_source}/media/icons/* ${dir_temp}/media/icons/ + +${dir_temp}/infra.tex: \ + $(wildcard ${dir_source}/tex/*) \ + ${dir_source}/data.json \ + $(wildcard ${dir_temp}/graphs/*) \ + tools/coin + @ ${cmd_log} "coining …" + @ tools/coin --data-path=${dir_source}/data.json ${coin_args} > ${dir_temp}/infra.tex + +${dir_temp}/infra.pdf: ${dir_temp}/infra.tex + @ ${cmd_log} "compiling …" + @ cd ${dir_temp} && ${cmd_latex} infra.tex && cd - > /dev/null + @ cd ${dir_temp} && ${cmd_latex} infra.tex && cd - > /dev/null + +${dir_build}/infra.pdf: ${dir_temp}/infra.pdf + @ ${cmd_log} "placing …" + @ ${cmd_mkdir} ${dir_build} + @ ${cmd_cp} ${dir_temp}/infra.pdf ${dir_build}/infra.pdf +