-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Use Case for Build Script Inversion: MicroZig #18808
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
It would be useful to define exactly what "inversion of control" means in the context of build scripts, because I'm not sure I understand what it means here. I completely get if something horrid like I'll chime in with a similar use case in my (WIP) bindings/wrapper library for the Playdate console, which also has some very specific constraints (embedded target, must use a specific linker script, must emit relocs, bundling binaries/assets using a proprietary SDK, debugging using a simulator, etc.) which require a lot of minute configuring and composing of build steps that would be very inconvenient to have to copy/paste into every consuming project. Through the power of const std = @import("std");
const playdate = @import("playdate");
pub fn build(b: *std.Build) void {
const pdex_targets = playdate.standardExecutableTargetOptions(b, .{});
const optimize = b.standardOptimizeOption(.{});
const pdx = playdate.addCompileBundle(b, .{});
for (pdex_targets) |target| {
const pdex = playdate.addCompileExecutable(b, .{
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
pdx.addExecutable(pdex);
}
pdx.addAsset(.{ .path = "assets/pdxinfo" }, "pdxinfo");
pdx.addAsset(.{ .path = "assets/Roobert-11-Mono-Condensed.fnt" }, "Roobert-11-Mono-Condensed.fnt");
pdx.addAsset(.{ .path = "assets/Roobert-11-Mono-Condensed-table-8-16.png" }, "Roobert-11-Mono-Condensed-table-8-16.png");
pdx.addAsset(.{ .path = "assets/sprites.png" }, "sprites.png");
const installed_pdx = playdate.addInstallBundle(b, pdx, .prefix, "MyGame.pdx");
b.getInstallStep().dependOn(&installed_pdx.installation.step);
const run_simulator = playdate.addRunSimulator(b, installed_pdx);
run_simulator.command.step.dependOn(b.getInstallStep());
const run_step = b.step("run", "Run the game in the Playdate Simulator");
run_step.dependOn(&run_simulator.command.step);
} I feel this is very elegant and I would be very sad if the build system is taken in a direction that makes usages like these no longer possible. |
on the term "inversion of control" EDIT: which IMO isn't really the core of this discussion
To my understanding of the Wikipedia definition, "inversion of control" is a phrase used for patterns where the callee calls code provided by the caller. I personally wouldn't call either of the examples posted here "inversion of control". EDIT:
I guess this should be the main point to focus on. (Just for completeness, it also looks like the packages |
Let's not get bogged down in the definition of "inversion of control". I'm sorry for using possibly overloaded language. We all know what is being discussed - only this exact pattern of @castholm the best I can possibly do, while making import functionality support playdate being a lazy dependency would be something like this: const std = @import("std");
-const playdate = @import("playdate");
pub fn build(b: *std.Build) void {
+ const playdate = b.importDependency("playdate") orelse return;
const pdex_targets = playdate.standardExecutableTargetOptions(b, .{});
const optimize = b.standardOptimizeOption(.{});
const pdx = playdate.addCompileBundle(b, .{}); "playdate" would need to be a comptime known value, and the function could only be called from the package that had playdate listed in build.zig.zon. Thanks for providing the examples, @MasterQ32 and @castholm. These use cases are certainly important and as far as I'm concerned any steps taken to change the build system must continue to address them satisfactorily. Just to be crystal clear, the changes in the recently merged PR (#18778) did nothing to regress these use cases. Rather, it means that those packages being used with |
What would the type of the |
It would be type const std = @import("std");
const assert = std.debug.assert;
extern fn side_effect(*i32) i32;
inline fn example(runtime_known: *i32, comptime x: bool) i32 {
if (x) {
return 1234;
} else {
return side_effect(runtime_known);
}
}
test "pass runtime param, receive comptime value" {
const ptr = try std.testing.allocator.create(i32);
defer std.testing.allocator.destroy(ptr);
const result = example(ptr, true);
comptime assert(result == 1234);
} This test passes, however, if you flip the
However... when I tried this example below I was actually surprised: const std = @import("std");
extern fn side_effect(*i32) void;
inline fn example(runtime_known: *i32, comptime x: bool) type {
if (x) {
return f32;
} else {
side_effect(runtime_known);
}
}
test "pass runtime param, receive comptime value" {
const ptr = try std.testing.allocator.create(i32);
defer std.testing.allocator.destroy(ptr);
const T = example(ptr, true);
try std.testing.expect(T == f32);
}
The problem here is that the return type forces the function call to be comptime, when we actually just wanted it to be inline. I will open a bug report for this. |
I believe this use case has been fully addressed by e204a6e. |
Uh oh!
There was an error while loading. Please reload this page.
This is a response to a comment from #18778:
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:
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.
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:
There are some questionable design decisions in there, but those could be changed to include more boilerplate, but would make some dependencies more explicit.
The text was updated successfully, but these errors were encountered: