- Rusts Abhängigkeitsverwaltungssystem macht die Entwicklung bequem, doch die Anzahl und Qualität der Abhängigkeiten bereiten Sorgen
- Selbst häufig genutzte Crates sind möglicherweise nicht aktuell, sodass eine eigene Implementierung manchmal besser sein kann
- Nach dem Hinzufügen bekannter Crates wie Axum und Tokio umfasst die gesamte Anzahl der Codezeilen inklusive Abhängigkeiten 3,6 Millionen und ist kaum noch beherrschbar
- Der von mir tatsächlich geschriebene Code umfasst nur rund 1.000 Zeilen, aber den umgebenden Code kann ich praktisch weder prüfen noch auditieren
- Für die Frage, ob Rusts Standardbibliothek erweitert werden sollte und wie zentrale Infrastruktur implementiert werden soll, gibt es keine klare Lösung; stattdessen muss die gesamte Community gemeinsam das Gleichgewicht zwischen Leistung, Sicherheit und Wartbarkeit ausloten
Überblick über das Rust-Abhängigkeitsproblem
- Rust ist meine Lieblingssprache, und Community und Nutzbarkeit der Sprache sind hervorragend
- Die Entwicklung ist produktiv, aber in letzter Zeit mache ich mir Sorgen wegen des Abhängigkeitsmanagements
Vorteile von Rust-Crates und Cargo
- Mit Cargo lassen sich Paketverwaltung und Build-Automatisierung erledigen, was die Produktivität stark verbessert
- Der Wechsel zwischen verschiedenen Betriebssystemen und Architekturen ist einfach, und man muss sich nicht um die manuelle Dateiverwaltung oder die Konfiguration von Build-Tools kümmern
- Man kann ohne zusätzliche Paketverwaltungsfragen direkt mit dem Schreiben von Code beginnen
Nachteile bei der Verwaltung von Rust-Crates
- Weil man sich weniger um Paketverwaltung kümmern muss, wird man bei der Stabilität leicht nachlässig
- So nutzte ich etwa das dotenv-Crate und erfuhr erst über ein Security Advisory, dass seine Wartung eingestellt worden war
- Ich zog ein Ersatz-Crate (
dotenvy) in Betracht, implementierte den tatsächlich benötigten Teil am Ende aber selbst in etwa 35 Zeilen
- Da Probleme mit nicht mehr gepflegten Paketen in vielen Sprachen häufig auftreten, liegt das eigentliche Problem in Situationen, in denen Abhängigkeiten unvermeidlich sind
Der sprunghafte Anstieg der Codebasis durch Abhängigkeiten
- Ich verwende wichtige und qualitativ hochwertige Pakete des Rust-Ökosystems wie Tokio und Axum
- Als Abhängigkeiten wurden Axum, Reqwest, ripunzip, serde, serde_json, tokio, tower-http, tracing und tracing-subscriber hinzugefügt
- Hauptzweck sind ein Webserver, das Entpacken von Dateien und Logging, daher ist das Projekt selbst einfach
- Mit der Cargo-vendor-Funktion wurden alle abhängigen Crates lokal heruntergeladen
- Eine Analyse der Codezeilen mit tokei ergab rund 3,6 Millionen Zeilen inklusive Abhängigkeiten (ohne vendorte Crates etwa 11.136 Zeilen)
- Zum Vergleich: Der gesamte Linux-Kernel soll etwa 27,8 Millionen Zeilen umfassen, mein kleines Projekt entspricht also rund einem Siebtel davon
- Der von mir tatsächlich geschriebene Code umfasst nur etwa 1.000 Zeilen
- So viele Zeilen an Abhängigkeitscode zu überwachen und zu auditieren ist praktisch unmöglich
Überlegungen zu Lösungen
- Derzeit gibt es keine klare Lösung
- Einige plädieren dafür, wie bei Go die Standardbibliothek zu erweitern, doch auch das schafft neue Probleme wie zusätzlichen Wartungsaufwand
- Rust strebt hohe Leistung, Sicherheit und Modularität an und will im Embedded-Bereich oder gegenüber C++ konkurrieren, daher muss eine Erweiterung der Standardbibliothek vorsichtig abgewogen werden
- Selbst ein hochentwickeltes Runtime-System wie Tokio wird beispielsweise auf GitHub und Discord sehr aktiv gepflegt
- Realistisch gesehen ist es für einzelne Entwickler zu viel verlangt, zentrale Infrastruktur wie asynchrone Runtimes oder Webserver selbst zu implementieren
- Selbst ein großer Dienst wie Cloudflare nutzt die Abhängigkeiten von tokio und crates.io unverändert; wie oft dort Audits stattfinden, ist unklar
- Auch Clickhouse hat Probleme mit Binärgröße und der Anzahl von Crates angesprochen
- Mit Cargo ist es schwer, die tatsächlich im finalen Binary enthaltenen Codezeilen exakt zu identifizieren, und zudem wird auch plattformspezifisch unnötiger Code einbezogen
- Letztlich bleibt nur die Realität, die Antwort von der gesamten Community einfordern zu müssen
3 Kommentare
Wenn man Trivy laufen lässt, gibt es im Vergleich zu js NPM oder Java Maven deutlich weniger High- oder Critical-Funde und es ist sicherer. Was genau will der Artikel also mit Rust behaupten?
Hacker-News-Kommentare
import foolibschreiben, und niemand kümmert sich darum, was darin eigentlich steckt. Auf jeder Ebene braucht man vielleicht nur etwa 5 % der Funktionalität, aber je tiefer der Baum wird, desto mehr nutzloser Code sammelt sich an. Am Ende wird aus einer einfachen Binärdatei 500 MiB, und man holt sich eine Abhängigkeit nur für Zahlenformatierung ins Haus. Go und Rust fördern es, alles in eine Datei zu packen, wodurch es schwierig wird, nur Teile davon zu nutzen. Langfristig ist die echte Lösung eine ultrafeingranulare Symbol-/Abhängigkeitsverfolgung, bei der jede Funktion und jeder Typ genau angibt, was benötigt wird, sodass nur der wirklich notwendige Code verwendet und der Rest verworfen wird. Persönlich finde ich diese Idee nicht besonders gut, aber mir fällt kein anderer Weg ein, das aktuelle System zu lösen, das aus dem Abhängigkeitsbaum gleich das ganze Universum heranziehtvulkan, PNG-Decoding, Unicode-Shaping usw.). Unnötige Abhängigkeiten waren meist sehr klein, und nurserde_jsonkonnte ich mit kleineren Änderungen entfernen. Größere Abhängigkeiten (winit/wgpuusw.) lassen sich ohne strukturelle Änderungen nicht einfach herausnehmen.o-Datei erzeugt und sie in ein.a-Archiv gepackt, aus dem der Linker dann nur die benötigten Funktionen herausgezogen hat. Namespacing lief über Dinge wiefoolib_do_thing(). Heute hat man eher das God-Object-Muster, bei dem ein Top-Level-Objekt alle Funktionen enthält, sodass beim Import vonfooliballes mit hereingezogen wird. In so einem Zustand kann der Linker nur schwer beurteilen, welche Funktionen wirklich notwendig sind. Go hat dafür eine hervorragende Dead-Code-Eliminierung, sodass ungenutzte Teile im Build-Ergebnis abgeschnitten werdenmin-sized-rustdeps-Datei hinzuzufügenisEven,isOdd,leftpadusw. bieten große universelle Bibliotheken, die von einem Verbundteam betreut werden, viel mehr Zukunftssicherheit und Kontinuität--gc-sectionsgelöstglob-Bibliothek einfach nur eine Funktion für Globbing sein, aber der Autor bündelt auch ein Kommandozeilenwerkzeug und fügt dafür einen großen Parser als Abhängigkeit hinzu. Das führt zu häufigen Warnungen wie „dependency out-of-date“. Außerdem ist auch der Verantwortungsbereich einerglob-Bibliothek umstritten. Nur String-Pattern-Matching anzubieten ist ein flexibleres Design, etwa für Tests oder Dateisystemabstraktion. Viele Nutzer wollen aber allmächtige „Do everything“-Bibliotheken, und genau das erhöht die Nebenwirkungen. Ich denke nicht, dass Rust sich darin stark unterscheidetstdlib::data_structures::automata::weighted_finite_state_transducer. Die Sprache selbst hat Versionsverwaltung und Abwärtskompatibilität eingebaut, daher erwarte ich weitere Verbesserungenglob-Funktion durchsucht tatsächlich das Dateisystem. Für String-Matching gibt esfnmatch. Idealerweise solltefnmatchein separates Modul sein, von demglobabhängt.globdirekt selbst zu implementieren ist ziemlich schwierig, weil Verzeichnisstrukturen, Brace Expansion und andere komplexe Anforderungen berücksichtigt werden müssen; man braucht also eine gut entworfene Kombination von Funktionen#![deny(unsafe_code)]wird die Verwendung vonunsafe-Code zu einem Compilerfehler und dem Nutzer kenntlich gemacht. Es ist aber keine allumfassende Erzwingung, denn mit ausdrücklicher Erlaubnis kann weiterhinunsafeverwendet werden. Man könnte sich die Einführung eines Capability-Systems vorstellen, das transitive Funktionen der Standardbibliothek ähnlich wie Feature-Flags steuerttls,x509,base64usw. bei Auswahl und Verwaltung von Bibliotheken schmerzhaft sindunsafe-Codedom0, jede Bibliothek in einer separaten Template-VM und Kommunikation über Netzwerk-Namespaces. Für sensible Branchen wäre das praktikabelblessed.rsempfiehlt eine Liste nützlicher Bibliotheken, die schwer in die Standardbibliothek aufgenommen werden können. Mir gefällt dieses System, weil die meisten Pakete dadurch auf einen bestimmten Zweck begrenzt und besser beherrschbar bleibencargo-vetist ebenfalls zu empfehlen. Damit lassen sich vertrauenswürdige Pakete nachverfolgen und definieren, etwa von Paketen, die vor der Aufnahme ein Expertenaudit brauchen, bis hin zu halb-YOLO-Regeln wie „Pakete, die von Tokio-Maintainern gepflegt werden, vertrauen wir einfach“. Es ist etwas formaler alsblessed.rsund eignet sich gut, um im Team eine offizielle Quasi-Standardliste zu teilencargo treelässt sich der Abhängigkeitsbaum leicht ansehen. Eine Ansicht der Codezeilen, die tatsächlich in der Binärdatei landen, ist dagegen nicht besonders aussagekräftig. Durch Function-Inlining landet das meiste ohnehin inmainDas ist nicht nur ein Problem von Rust.
Es ist ein gemeinsamer Vorteil und zugleich ein potenzielles Problem aller Sprachen, die öffentliche Paket-Repositories und Paketmanager mit transitiven Abhängigkeiten unterstützen.
Letztlich müssen die Leute, die es verwenden, eben gut damit umgehen …
Trotz des
leftpad-Vorfalls bei Node&npm hat sich nichts geändert.