Description
This is a response to a comment from #18778:
direct
@import
in build scriptsI looked into making this work for lazy dependencies. It's possible, but it has some downsides:
- it requires generating a zig source file per dependency instead of one dependencies.zig file for the whole tree. I don't know how much of a problem this is in reality, perhaps it's more distasteful in theory than in practice.
- It requires some clunky API in the build.zig script - an explicit check for the existence of a dependency followed by the
@import
, or something like that.Maybe I could find a more satisfactory solution if I keep poking at it.
That said, this feature was always meant for actual build script dependencies, it was not intended for inversion of control and having the dependency take over the build script logic for the parent package. I think users should try to get away from doing that, and I'm happy to work together on coming up with a satisfactory API that is based on dependencies exposing things rather than doing build logic.
MicroZig currently heavily relies on this kind of build script inversion, as it tries to provide a similar experience to the user as a regular desktop build script does.
Tasks that need to be done for building a single executable for an embedded task usually require the following steps:
- Generate/write fitting linker script that is basically the same for all MCUs except for a hand full of offsets and lengths.
- (optionally) Compile a bootloader
- Compile the main application. This depends on modules for:
- Application (this is what the user wants to write)
- (optional) Driver Packages (which the user wants to include)
- Boot Logic (depending on the previously compiled bootloader, if any)
- CPU support
- MCU support
- (optional) Board support
- (optional) Hardware Abstraction Layer
- (optional) Patch the resulting ELF file to insert checksums, boot marks, …
- (optional) Convert the ELF file from step 4. into another format like UF2, Intel Hex, DFU, …
- Depends on external tooling and libraries like ZIP, …
Usually the build script also has features to flash the target CPU, which also requires additional tooling and build steps.
The whole idea of MicroZig is to offload the boilerplate into the MicroZig package and let the user focus on what they care about:
Application code and optional driver/package dependencies
Everything else is basically 100% repetetive between projects and doesn't differ between basically all build processes.
That said, this feature was always meant for actual build script dependencies, it was not intended for inversion of control and having the dependency take over the build script logic for the parent package. I think users should try to get away from doing that, and I'm happy to work together on coming up with a satisfactory API that is based on dependencies exposing things rather than doing build logic.
Right now, MicroZig has over 2000 lines of code for build automation, which don't have much "logic" inside them glueing different steps and modules together to solve the tasks above.
I would rather not have to force every user to have something like 1500 LOC of build script in their embedded projects, because this makes them really hard to maintain, as copy-paste errors start to emerge over time.
@andrewrk, i'm happy to find a solution to make build scripts/automation simpler in the future that could work without having build script inversion, but i'm not sure how to do that.
My goal is that we can keep the build scripts as simple as they are right now:
const std = @import("std");
const MicroZig = @import("microzig-build");
pub fn build(b: *std.Build) void {
const microzig = MicroZig.createBuildEnvironment(b, .{});
const show_targets_step = b.step("show-targets", "Shows all available MicroZig targets");
show_targets_step.dependOn(microzig.getShowTargetsStep());
const optimize = b.standardOptimizeOption(.{});
const target_name = b.option([]const u8, "target", "Select the target to build for.") orelse "board:mbed/lpc1768";
const target = microzig.findTarget(target_name).?;
const firmware = microzig.addFirmware(b, .{
.name = "blinky",
.target = target,
.optimize = optimize,
.source_file = .{ .path = "src/empty.zig" },
});
microzig.installFirmware(b, firmware, .{});
}
There are some questionable design decisions in there, but those could be changed to include more boilerplate, but would make some dependencies more explicit.