Skip to content

Conversation

@MHajoha
Copy link
Member

@MHajoha MHajoha commented Jan 13, 2026

Der handgeschriebenen Algorithmus, den ich in einem der letzten Meetings letztes Jahr erwähnt hatte, war zwar schön und hätte viele Fälle korrekt berechnet, ich bin aber doch noch auf Edge-Cases gestoßen, die damit nicht sinnvoll zu lösen waren. Stattdessen habe ich jetzt zu resolvelib, die auch von pip verwendet wird, gegriffen. Das macht die Implementierung nicht weniger kompliziert, aber dafür richtig :)

Kurzer Abriss zu resolvelib:

  • Die meisten resolvelib-Klassen sind generisch über RT (Den Requirement-Typen), CT (Den Lösungskandidat-Typen) und KT (den Identifier Typen)
    • RT ist bei uns DynamicRequirement | StaticRequirement | RootRequirementAndCandidate, wobei die ersteren beiden nur Wrapper um DistDynamicQPyDependency bzw. DistStaticQPyDependency mit schönem __str__ sind.
    • CT ist bei uns DynamicCandidate | StaticCandidate | RootRequirementAndCandidate, wobei die ersteren beiden Aliase der neuen Klassen DynamicDependencySolution und StaticDependencySolution sind, die in quesionpy_common leben, weil sie auch ans Paket geschickt werden.
    • KT ist bei uns PackageNamespaceAndShortName
  • Der QPyResolvelibProvider implementiert alle domänenspezifische Funktionalität, die resolvelib braucht:
    • identify gibt bei uns ganz trivial die NSSN zurück
    • get_preference ist eine Heuristik und gibt für je ein Paket einen Sort-Key zurück, der dafür sorgt, dass stärker eingeschränkte Pakete zuerst resolved werden. (z.B. statische Dependency vor @local/dep_a == 1.2.3 vor @local/dep_b ^= 1.0.0 vor @local/dep_c)
    • find_matches findet für ein Paket (NSSN) alle Lösung, die die bisher gefundenen Requirements erfüllen, geordnet von der "besten" (neusten) zur "schelchtesten" (ältesten).
    • is_satisfied_by prüft, ob ein irgendwann bereits von `find_matches) gefundener Kandidat das gegebene (vllt. neue) Requirement erfüllt.
    • get_dependencies gibt die transitiven Dependencies eines Candidates zurück, wickelt bei uns also einfach DynamicRequirement/StaticRequirement um die DistQPyDependency
  • Der QPyResolvelibReporter wird bei den einzelnen Schritten im resolvelib-Algorithmus informiert. Ich mache damit zwei Dinge:
    • Ich sammle Meldungen in einer Liste und gebe eine gebündelte Lognachricht nach der Resolution aus. Schlägt die fehl, wird alles auf INFO geloggt. Ist sie erfolgreich, wird entweder alles auf DEBUG oder nur eine Zusammenfassung auf INFO geloggt.
    • Nach erfolgreichem Ende prüfe ich auf Zyklen (mit denen resolvlib grundsätzlich klar kommt, wir aber nicht) und prüfe, ob MAX_QPY_DEPENDENCY_LEVELS überschritten wurde. (Das ist vermutlich nicht ganz so gedacht, aber pragmatisch.)

(Zu RootRequirementAndCandidate: Das Root-Paket muss extra abgebildet werden, weil resolvelib sonst keine Konflikte damit erkennen kann. Da es genau ein Root-Requirement und einen ihm gleichen Root-Kandidaten gibt, benutze ich eine Klasse für beides.)

Erwähnenswert ist vielleicht auch, dass ein DynamicRequirement durchaus durch einen StaticCandidate gelöst werden kann, wenn ein anderes Paket diese statische Dependency hat und die beiden sich nicht widersprechen.

Die Implementierung des resolvelib-Providers und -Reporters, sowie eine kleine Wrapper-Funktion liegen in questionpy_server.dependencies._solver. Das ist explizit so gedacht, dass es sowohl vom Server beim Ausführen als auch vom SDK beim Bauen (und Ausführen) verwendet werden kann.

Da sich die PackageCollection aus dem Server nicht wirklich für's SDK eignet, ist die Paket-Abfrage selbst in der ABC DynamicDependencyResolver abstrahiert.

Bei Worker-Start werden nach der Auflösung des Baumes für alle ausgewählten Lösungen die PackageLocations geholt, und in der LoadQPyPackage-Nachricht an den Worker geschickt. An der Logik im Worker hat sich nicht super viel verändert, nur werden Pakete jetzt nicht mehr rekursiv geöffnet, sondern in umgekehrter topologischer Sortierung.

TODO

Die Lock-Informationen, die vom SDK jetzt schon geschrieben werden, auch verwenden. Angesichts der Zeit habe ich das jetzt außen vor gelassen.

ManifestFactory isn't available in the class scope.
It was the only route not using it.
Instead of dir_name, specify NS, SN and version. The dir name will later
be built on the fly.

Add transitive dependencies so that a complete dependency tree can be
built without retrieving manifest of the statically packaged dependency.
Overriding `version: str` with the incompatible `semver.Version` type breaks the Liskov substitution principle and leads to problems when trying to write code that supports both `Manifest` and `PackageConfig`.
resolve_dependency_tree is intended to also be usable by the SDK when validating and locking the dependency tree.
This led to an unhelpful InconsistentCandidate error before.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants