Zig Version
0.15.2
Steps to Reproduce and Observed Behavior
I'll post a minimal reproduction below, but here's the background first:
Background
We've been frequently seeing errors like this one in Roc CI:
Cross-compiling int platform from macos-15 to x64musl
ld.lld: error: test/int/./platform/targets/x64musl/libhost.a: Archive::children failed: truncated or malformed archive (offset to next archive member past the end of the archive after member host.o)
I believe what's happening is that lld is rejecting this because, per the man page for ar: (emphasis mine)
Each archive file member begins on an even byte boundary; a newline is inserted between files if necessary. Nevertheless, the size given reflects the actual size of the file exclusive of padding.
I think that currently (as of Zig 0.15.2), if the final object file being emitted into the archive happens to have a size with an odd number of bytes, the required single newline worth of padding is not being added, which means the next file begins on an odd byte number and lld rejects the whole archive.
Minimal Reproduction
Make a directory with these 3 files in it:
lib.zig
export fn add(a: i32, b: i32) i32 {
return a + b;
}
build.zig
const std = @import("std");
pub fn build(b: *std.Build) void {
const lib = b.addLibrary(.{
.name = "testlib",
.linkage = .static,
.root_module = b.createModule(.{
.root_source_file = b.path("lib.zig"),
.target = b.resolveTargetQuery(.{
.cpu_arch = .x86_64,
.os_tag = .linux,
.abi = .musl,
}),
.optimize = .Debug,
}),
});
b.installArtifact(lib);
}
build.zig.zon
.{
.name = .zig_ar_test,
.version = "0.0.0",
.fingerprint = 0xc6af8bb730274257,
.paths = .{""},
}
Then from inside that directory, run:
zig build
zig ld.lld -r -o out.o zig-out/lib/libtestlib.a
When I do this on an aarch64 Mac (so, this is cross-compiling - I'm not sure if cross-compiling is required to reproduce this, but I suspect it would behave the same way if you run this on x86_64 Linux), I see:
ld.lld: error: zig-out/lib/libtestlib.a: Archive::children failed: truncated or malformed archive (offset to next archive member past the end of the archive after member lib.o)
Expected Behavior
lld should exit with status 0.
Since this archive only has a single file, there's a very quick way to verify that this newline-padding is the fix:
cp zig-out/lib/libtestlib.a zig-out/lib/fixed.a
printf '\n' >> zig-out/lib/fixed.a
zig ld.lld -r -o out.o zig-out/lib/fixed.a
When I do that, it passes.
Note: My understanding from the ar docs (linked in the Steps to Reproduce section) is that each file in the archive needs this padding if it happens to have an odd number of bytes, so I don't think appending a single newline to the end of the archive would suffice in the general case - it's just that for this specific example of one file it's enough.
### Zig Version
0.15.2
### Steps to Reproduce and Observed Behavior
I'll post a minimal reproduction below, but here's the background first:
**Background**
We've been frequently seeing errors like [this one in Roc CI](https://github.com/roc-lang/roc/actions/runs/20442587942/job/58738904218?pr=8712#step:8:9):
```
Cross-compiling int platform from macos-15 to x64musl
ld.lld: error: test/int/./platform/targets/x64musl/libhost.a: Archive::children failed: truncated or malformed archive (offset to next archive member past the end of the archive after member host.o)
```
I believe what's happening is that lld is rejecting this because, per [the man page for ar](https://docs.oracle.com/cd/E36784_01/html/E36873/ar-3head.html): (emphasis mine)
> Each archive file member begins on an **even byte boundary**; a newline is inserted between files if necessary. Nevertheless, the size given reflects the actual size of the file exclusive of padding.
I think that currently (as of Zig 0.15.2), if the final object file being emitted into the archive happens to have a size with an odd number of bytes, the required single newline worth of padding is not being added, which means the next file begins on an odd byte number and lld rejects the whole archive.
**Minimal Reproduction**
Make a directory with these 3 files in it:
lib.zig
```
export fn add(a: i32, b: i32) i32 {
return a + b;
}
```
build.zig
```
const std = @import("std");
pub fn build(b: *std.Build) void {
const lib = b.addLibrary(.{
.name = "testlib",
.linkage = .static,
.root_module = b.createModule(.{
.root_source_file = b.path("lib.zig"),
.target = b.resolveTargetQuery(.{
.cpu_arch = .x86_64,
.os_tag = .linux,
.abi = .musl,
}),
.optimize = .Debug,
}),
});
b.installArtifact(lib);
}
```
build.zig.zon
```
.{
.name = .zig_ar_test,
.version = "0.0.0",
.fingerprint = 0xc6af8bb730274257,
.paths = .{""},
}
```
Then from inside that directory, run:
```
zig build
zig ld.lld -r -o out.o zig-out/lib/libtestlib.a
```
When I do this on an aarch64 Mac (so, this is cross-compiling - I'm not sure if cross-compiling is required to reproduce this, but I suspect it would behave the same way if you run this on x86_64 Linux), I see:
```
ld.lld: error: zig-out/lib/libtestlib.a: Archive::children failed: truncated or malformed archive (offset to next archive member past the end of the archive after member lib.o)
```
### Expected Behavior
lld should exit with status 0.
Since this archive only has a single file, there's a very quick way to verify that this newline-padding is the fix:
```
cp zig-out/lib/libtestlib.a zig-out/lib/fixed.a
printf '\n' >> zig-out/lib/fixed.a
zig ld.lld -r -o out.o zig-out/lib/fixed.a
```
When I do that, it passes.
**Note:** My understanding from the ar docs (linked in the Steps to Reproduce section) is that each file in the archive needs this padding if it happens to have an odd number of bytes, so I don't think appending a single newline to the end of the archive would suffice in the general case - it's just that for this specific example of one file it's enough.