Why standardTargetOptions target os is different from my OS?

Hi, zig noob here.
I’m trying to use zig as a build system for my cpp project.

So i’m tinkering with build.zig, and I thought std.Build.standardTargetOptions would return the native os of the computer running the build command.

But I got aix instead of windows. I thought this is because I’m using mingw, so I also tried PowerShell and window’s defualt cmd, but nothing changed.

My build command is just zig build run nothing more. Also tried zig build -Dtarget=x86_64-windows, but it remains same.

When I checked os using builtin.os.tag, it returns windows.

Should I have to use builtin.os.tag instead of std.Build.standardTargetOptions().result.os.tag?

Then why standardTargetOptions even exists?

Here is my build.zig file.

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const mode = b.standardOptimizeOption(.{ .preferred_optimize_mode = .ReleaseFast });

    const exe = b.addExecutable(.{
        .name = "softy",
        .target = target,
        .optimize = mode,
    });
    exe.addCSourceFiles(.{ .files = &.{
        "src/main.cpp",
    }, .flags = &.{
        "-std=c++23",
        "-Wall",
        "-Wextra",
        "-Wshadow",
        "-Wnon-virtual-dtor",
        "-pedantic",
        "-Wno-gnu-anonymous-struct",
        "-Wno-nested-anon-types",
        "-Wformat",
        "-Wformat=2",
        "-Wconversion",
        "-Wimplicit-fallthrough",
        "-Werror=format-security",
        "-D_FORTIFY_SOURCE=3",
        "-fstrict-flex-arrays=3",
        "-fstack-clash-protection",
        "-fstack-protector-strong",
        "-fPIE",
    } });
    exe.addIncludePath(b.path("./src"));
    exe.linkLibCpp();

    switch (target.result.os.tag) {
        .windows => {
            exe.addCSourceFile(.{ .file = b.path("src/window/window_win32.cpp") });
            exe.linkSystemLibrary("gdi32");
        },
        .linux => {},
        .macos => {},
        else => @compileError("Unsupported OS"),
    }

    b.installArtifact(exe);

    const run_cmd = b.addRunArtifact(exe);
    run_cmd.step.dependOn(b.getInstallStep());

    const run_step = b.step("run", "Run the app");
    run_step.dependOn(&run_cmd.step);
}

The way you’re doing it looks correct to me, and it should be returning windows, I’m not sure why you’re getting aix.

I wondered if you might (somehow) be getting a corrupted value, however I’m not sure how. I don’t think it’s getting overwritten with undefined bytes which are then interpreted as aix, at the very least.

May I ask how you know that you’re getting aix instead of windows?

I will note that b.standardTargetOptions(.{}) the target to compile for, not necessarily the target of the computer running the build command. To get the ResolvedTarget for the build host, use b.graph.host.

builtin.os.tag gives you the target of the build script, which may not be the target for the build host, if something like Run build.zig logic in a WebAssembly sandbox · Issue #14286 · ziglang/zig · GitHub ever gets implemented.

Thank you for the reply.

I just added more cases to the switch expression, and it hits the aix.

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const mode = b.standardOptimizeOption(.{ .preferred_optimize_mode = .ReleaseFast });

    const exe = b.addExecutable(.{
        .name = "softy",
        .target = target,
        .optimize = mode,
    });
    exe.addCSourceFiles(.{ .files = &.{
        "src/main.cpp",
    }, .flags = &.{
        "-std=c++23",
        "-Wall",
        "-Wextra",
        "-Wshadow",
        "-Wnon-virtual-dtor",
        "-pedantic",
        "-Wno-gnu-anonymous-struct",
        "-Wno-nested-anon-types",
        "-Wformat",
        "-Wformat=2",
        "-Wconversion",
        "-Wimplicit-fallthrough",
        "-Werror=format-security",
        "-D_FORTIFY_SOURCE=3",
        "-fstrict-flex-arrays=3",
        "-fstack-clash-protection",
        "-fstack-protector-strong",
        "-fPIE",
    } });
    exe.addIncludePath(b.path("./src"));
    exe.linkLibCpp();

    switch (target.result.os.tag) {
        .windows => {
            exe.addCSourceFile(.{ .file = b.path("src/window/window_win32.cpp") });
            exe.linkSystemLibrary("gdi32");
        },
        .linux => {},
        .macos => {},
        .aix => @compileError("aix"),
        .amdhsa => @compileError("amdhsa"),
        .freebsd => @compileError("freebsd"),
        else => @compileError("Unsupported OS"),
    }

    b.installArtifact(exe);

    const run_cmd = b.addRunArtifact(exe);
    run_cmd.step.dependOn(b.getInstallStep());

    const run_step = b.step("run", "Run the app");
    run_step.dependOn(&run_cmd.step);
}

Oh, you’re running into a different issue here. TL;DR swap @compileError() for @panic().

If you remove the case for .aix you’ll get an error for the next case:

~/tmp> diff -u build1.zig build.zig
--- build1.zig	2025-08-14 22:58:05.679692691 -0600
+++ build.zig	2025-08-14 22:58:41.475920407 -0600
@@ -41,7 +41,6 @@
         },
         .linux => {},
         .macos => {},
-        .aix => @compileError("aix"),
         .amdhsa => @compileError("amdhsa"),
         .freebsd => @compileError("freebsd"),
         else => @compileError("Unsupported OS"),
~/tmp> zig build
/home/geemili/tmp/build.zig:44:20: error: amdhsa
        .amdhsa => @compileError("amdhsa"),
                   ^~~~~~~~~~~~~~~~~~~~~~~
referenced by:
    runBuild__anon_24623: /home/geemili/.local/share/ziglang/0.14.1/lib/std/Build.zig:2427:33
    main: /home/geemili/.local/share/ziglang/0.14.1/lib/compiler/build_runner.zig:339:29
    5 reference(s) hidden; use '-freference-trace=7' to see all references

The reason for this is because you are getting this error while compiling build.zig.

It works when you use builtin.os.tag because builtin.os is know at compile time. When you use b.standardTargetOptions(), it isn’t know until runtime. However Zig still analyzes the rest of the function, and when it finds a @compileError() in a run-time reachable codepath, it aborts the compilation.

The simplest solution is to replace the @compileError() with a @panic(). This will tell Zig to place some code at runtime that prints out whatever message you give to @panic(). Better yet you could use std.debug.panic() to print out the unsupported os:

switch (target.result.os.tag) {
    else => |os_tag| std.debug.print("Unsupported OS: {s}", .{@tagName(os_tag)}),
}

Or you could instead use the whitelist option to ensure that the target os is in an allowed list of targets:

    const target = b.standardTargetOptions(.{
        .whitelist = &.{
            .{ .os_tag = .windows },
            .{ .os_tag = .macos },
            .{ .os_tag = .linux },
        },
    });
4 Likes

Thanks for the help! It worked perfectly.

It was caused by the @compileError.

I thought it was just a way to print an error message and stop the compiler right there. Like run through line by line and when it hit, it fires or something.

There are a couple phases that happen when running zig build:

  1. Compile build.zig. This will produce an executable named something like ./.zig-cache/o/f8dcf2babc10726618c03e7b324ad925/build
  2. Run the build executable. This will run fn build(), where you build up a description of the steps required to build the actual project.
  3. Compile the actual project. If you run zig build --verbose you can see the actual zig compiler invocation that is used to compile the project. For example:
    ~/tmp> zig build --verbose
    /home/geemili/.local/share/ziglang/0.14.1/zig build-exe -cflags -std=c++23 -Wall -Wextra -Wshadow -Wnon-virtual-dtor -pedantic -Wno-gnu-anonymous-struct -Wno-nested-anon-types -Wformat -Wformat=2 -Wconversion -Wimplicit-fallthrough -Werror=format-security -D_FORTIFY_SOURCE=3 -fstrict-flex-arrays=3 -fstack-clash-protection -fstack-protector-strong -fPIE -- /home/geemili/tmp/src/main.cpp -ODebug -I /home/geemili/tmp/src -Mroot -lc++ --cache-dir /home/geemili/tmp/.zig-cache --global-cache-dir /home/geemili/.cache/zig --name softy --zig-lib-dir /home/geemili/.local/share/ziglang/0.14.1/lib/ --listen=- 
    

I thought it was just a way to print an error message and stop the compiler right there. Like run through line by line and when it hit, it fires or something.

I think you are expecting @compileError to stop the compiler during phase 2, when in reality the @compileError occurs during phase 1. I can see how the confusion might occur. It might be useful to keep in mind that a build.zig program is just a zig program that follows a different convention than a typical fn main() program.

You might also check out Zig Build System ⚡ Zig Programming Language

4 Likes