SDL_image, SDL and transitive dependencies

Hello everyone,

I’m currently learning zig, zig build and drinking from two fire hoses at the same time.

I’m trying to build an existing C project that depends on SDL2 and SDL2_image and I’d like to use what’s already done in allyourcodebase.

SDL_image declares SDL as a dependency. So, I have two questions:

  1. Since the C project I’m compiling depends on both SDL and SDL_image, is it better to declare both as dependencies in the C project, or to just rely on the transitive dependency of SDL_image on SDL

  2. In general, is there a current idiom for controlling the compilation of transitive dependencies? For example, I want to compile SDL2 but with a change to default options:

const sdl_dep = b.dependency("sdl", .{ .render_driver_ogl_es = false });

but as far as I can see, if I do not wish it include SDL as a dependency directly I cannot control the options SDL_image uses when building SDL. Or, alternatively, is there a way to tell SDL_image: “Hey, I know you depend on SDL2 - used this one instead of the one you call out in your build.zig.zon, I’ll take care of fetching and compiling it”?

1 Like

zig can recognise shared dependencies, depend on the same version for convenience, since sdl_image doesn’t seem to re expose sdl.

To put it simply, the Zig package ecosystem at large hasn’t really figured out how to solve this problem with transitive dependencies that the root package might want to configure. Some packages will redeclare the same options as the transitive dependency and forward them, but that can only get you so far and doesn’t let the root package e.g. swap out the transitive dependency for a fork or some other kind of substitute.

For something like SDL_image that only really cares about the SDL library binary and its headers, I think the ideal solution would be to expose two lazy path options:

const sdl_library_path = b.option(
    std.Build.LazyPath,
    "sdl_library_path",
    "Path to the SDL library file",
);
const sdl_headers_path = b.option(
    std.Build.LazyPath,
    "sdl_headers_path",
    "Path to the directory containing the SDL headers",
);

// ...

if (sdl_library_path) |x| my_root_module.addObjectFile(x);
if (sdl_headers_path) |x| my_root_module.addIncludePath(x);

This gives the root package full control over the SDL dependency. You can both build it yourself using the Zig build system and pass the lazy path to the compiled artifact and its header tree

const sdl_dep = b.dependency("sdl", .{ ... });
const sdl_lib = sdl_dep.artifact("SDL3");
const sdl_image_dep = b.dependency("sdl_image", .{
    .sdl_library_path = sdl_lib.getEmittedBin(),
    .sdl_headers_path = sdl_lib.getEmittedIncludeTree(),
});
const sdl_image_lib = sdl_image_dep.artifact("SDL3_image");

or just pass a static path to a pre-compiled SDL library path that is managed entirely outside of the build system

const sdl_image_dep = b.dependency("sdl_image", .{
    .sdl_library_path = b.path("vendor/sdl/libSDL3.so"),
    .sdl_headers_path = b.path("vendor/sdl/include"),
});
const sdl_image_lib = sdl_image_dep.artifact("SDL3_image");

If the build system could be extended to let you pass artifacts and modules as options, and potentially make it possible to “reify” a std.Build.Step.Compile from a path, that would be even better.

3 Likes

Thanks @castholm! I want to make sure I understand you correctly.

This would require both my package’s build.zig and the SDL_image to declare two options, and I would pass these options into the SDL_image build via the std.Build.dependency(…)? That is assuming I want to specify it at the top level at the command line. If I just want to handle it the build.zig, then only in the SDL_image build.zig.

The current SDL_image from allyourcodebse declares the allyourcodebase SDL as a dependency. So, perhaps it would also be good to only add it as a dependency in SDL_image.zig if the options were not passed in (or more specifically, I think, if the options are null)? It seems wasteful to fetch and build that if the top-level is taking on that responsibility.

Yes, that’s what I would do, mark the SDL dependency as .lazy = true in the build.zig.zon and use b.lazyDependency() behind a condition that checks whether the path options are null. That way it will only be fetched and used as the default fallback if the consumer doesn’t pass any paths.

Cool, thanks. I may take a crack at adding the ability to pass a dependency list in as a user option to std.Build.dependency just for the sake of learning.