Injecting the right version of a transitive package dependency?

I have the following dependency situation:

  • “game” - my main application
    → depends on microui
    → depends on sokol
  • “microui” - a GUI library
    → backend portion depends on sokol

This is working, even in the case when “sokol” is not listed at the same hash in “microui” and “game” build.zig.zon files.

However, if I call functions from “microui” that use “sokol” types, like in this shortened example:

// `sapp.Event` is a type defined in the sokol package
export fn input(event: ?*const sapp.Event) void {
    ... 
    microui.sokol.handleEvent(&engine.mui_ctx, ev);
}

I’m met with the following error message:

src/main.zig:201:48: error: expected type '*const app.Event', found '*const app.Event'
    microui.sokol.handleEvent(&engine.mui_ctx, ev);
                                               ^~
src/main.zig:201:48: note: pointer type child 'app.Event' cannot cast into pointer type child 'app.Event'

It’s not too hard to understand why this is not ok, the problem seems to be that “microui” uses a different version of the “sokol” dependency than “game”:

/Users/nurpax/.cache/zig/p/sokol-0.1.0-pb1HK_ZPLgC-2uMA63u1iiB2BqPcibWOEZVH0Hz1ursz/src/sokol/app.zig:1551:26: note: struct declared here
pub const Event = extern struct {
                  ~~~~~~~^~~~~~
/Users/nurpax/.cache/zig/p/sokol-0.1.0-pb1HK4jQLQD-hG8KuP-i7qkA5RY5pVfL7hLg56FlckV0/src/sokol/app.zig:1526:26: note: struct declared here
pub const Event = extern struct {
                  ~~~~~~~^~~~~~
/Users/nurpax/.cache/zig/p/microui-0.1.0-BtZcAmAGBABhWn7mXFnx_pH5Nbjq8HZpYKE1nriCA7Wp/src/sokol/backend.zig:129:39: note: parameter type declared here
pub fn handleEvent(ctx: *Context, ev: *const sapp.Event) void {
                                      ^~~~~~~~~~~~~~~~~

The code compiles if I match the “sokol” version in both the “microui” and “game” projects.

Question: Is it possible to inject, or somehow pass the “sokol” module/dependency from my “game” project’s build.zig for “microui” to use? This way I could support a wider range of “sokol” versions in my projects.

if you use the same version (hash) than you wont have this issue.

you can also grab the dependencies of your dependencies

const microui = b.dependency("microui", .{..});
const sokol = microui.builder.dependency("sokol", .{..});

they are effectively the same.

make sure you pass the same args to sokol that microui does.

Thanks! I did notice it works with the same hash – it’s working fine now and since microui is my own package, I can easily match the hashes. For a larger ecosystem of packages though, always requiring exact hashes may be quite restrictive.

you can also grab the dependencies of your dependencies

I wonder if it’s possible for this to go the other way? Ie., my app’s build.zig could specify what version of sokol the microui package uses? Upgrading my app’s direct deps just because some dependent library has upgraded sokol is a big ask. And vice verse, it’d suck for my app to have to stay at some older version, just because the microui is using an older version of sokol.

Its no different than matching version, though it is a far more precise version match.

In the future, zig will figure out the proper version to use when a package is depended on multiple times using semver, but that just isn’t implemented as there are more important things to work on.

possible? yes, recommended? :person_shrugging:.

same way you depend on packaged already, just you have to provide the imports that is usually done for you.

If you go that route, I recommend making a helper function in the dependencies build.zig which you can get by @import("dep_name").helperFn. This would ensure all imports are provided, and that the import name isn’t typo’d.

Its no different than matching version, though it is a far more precise version match.

I don’t mind that I have to compile against the exact same version of a dependency. It’s just that I’d like there to be a clear way of specifying what this version is in my root project. It’ll get messy otherwise if I need to fork github repos in order to set their library dep versions.

that’s why you and others should depend on ‘released’ versions, instead of some random point in git history. Makes it more likely packages depend on the same version, likely the latest ‘release’.

Unfortunately, due to zig not yet figuring out the common package version among dependencies, if there is a miss match between dependencies you will have to fork one of them to fix it.

If it’s your root project that’s miss matched, than you can just copy the dependency from your dependency, or access it through your dependency.

I don’t disagree but I also don’t think there’s only “One True Way” of doing this. I’m simply looking for pragmatic churn-avoidance techniques. In this case, the dependency in question is sokol gfx, and I’m not aware of any versioned releases of this header library. Also I don’t count that as a fault of sokol, they’re allowed to release their code in any form they wish.

Similar churn-avoidance techniques are f.ex. many libraries supporting both zig-dev and zig-0.15 (or even zig-0.14, zig-0.15 and -dev) simultaneously. I really like it that some projects go through this effort, as it enables me to pick up newer versions of such libraries, while not being forced to upgrade zig (potentially just to a new git master version) at any point any of my libraries get upgraded. Sure I try to keep things mostly up to date, but there are times when you just want to focus on your own project instead of doing chores.

One problem is that I haven’t figured out how versioning would fit into sokol and sokol-zig, because:

  • sokol is essentially a library collection, each library in the collection would need its own version number (especially when using semantic versioning - e.g. a if there’s a breaking change in sokol-gfx but no changes to other libraries, only the sokol-gfx version should be bumped)

  • the sokol-zig bindings are updated automatically on each commit to the sokol repository, and there’s no version bumping happening to build.zig.zon (because: bump to which version when the parent project has no versioning at all - and if sokol would introduce a per-library versioning, this couldn’t be merged into a single build.zig.zon version number either)

In general it would be great though if the package manager would offer a way to overrride the version of subdependencies, or fail the build if the same dependency is used in different versions throughout the dependency tree.

Or better: add a proper peer-dependency-feature - e.g. a Zig package might declare a dependency as a peer-dependency which means that the top-level build.zig.zon must povide the peer dependencies for the whole dependency tree.

E.g. let’s say there’s a microui-sokol package which depends on the sokol-zig package. sokol-zig would be declared as peer dependency in the microui-sokol build.zig.zon (optionally with a range of compatible versions).

Then a top-level project wants to use microui-sokol so it adds it to its build.zig.zon. The build would now fail with an ‘unresolved peer dependency’ error. To fix this, a sokol dependency must be added to the top-level build.zig.zon as well, and this version would be used by all other dependencies which declare sokol as peer dependency.

2 Likes