Skip to content

Tap formula with using: :nounzip is treated as a tarball (can1357/tap/omp) #361

Description

@StayBlue

Summary

nb install can1357/tap/omp downloads and verifies the correct macOS arm64 release asset, but treats the raw executable as a tarball and fails with tar: Error opening archive: Unrecognized archive format.

The same can1357/tap/omp formula installs normally with Homebrew; brew info shows 17.1.6 installed and linked on this machine.

Environment

  • nanobrew: v0.1.205
  • macOS: 26.5.2 (25F84)
  • architecture: arm64
  • nanobrew current main also inspected at 308986a79bb706b4924100ffd0f51a59c02a4f61
  • tap formula inspected at can1357/homebrew-tap@9dec8a6847a6c8607d707d9c6230d5b56051bd59

Reproduction

With no Nanobrew omp keg or install history:

$ nb install can1357/tap/omp
==> Resolving dependencies...
    [183ms]
==> Installing 1 package(s) (0 already up to date):
    omp 17.1.6
==> Downloading + installing 1 packages...
==> Downloading source for omp 17.1.6...
    https://github.com/can1357/oh-my-pi/releases/download/v17.1.6/omp-darwin-arm64
==> Verifying SHA256...
==> Extracting source...
tar: Error opening archive: Unrecognized archive format
nb: extract command failed: error.CommandFailed
nb: omp: source build failed: error.ExtractFailed
    ✗ omp (source build failed)
nb: some packages failed to install
    [112017ms]
    failed: omp (source build failed)
nb: hint: check permissions with `nb doctor`
==> Done in 112205.4ms

The process also exited with status 0 despite reporting that the package failed to install.

Evidence / likely cause

The formula explicitly declares the release assets as uncompressed:

url "https://github.com/can1357/oh-my-pi/releases/download/v#{version}/omp-darwin-arm64",
    using: :nounzip

Formula: https://github.com/can1357/homebrew-tap/blob/9dec8a6847a6c8607d707d9c6230d5b56051bd59/Formula/omp.rb#L7-L11

The file Nanobrew cached as omp-17.1.6.tar.gz is actually the expected raw executable:

$ file /opt/nanobrew/cache/tmp/omp-17.1.6.tar.gz
/opt/nanobrew/cache/tmp/omp-17.1.6.tar.gz: Mach-O 64-bit executable arm64

$ shasum -a 256 /opt/nanobrew/cache/tmp/omp-17.1.6.tar.gz
6c0c45af6c8c566ce28370c8570d9a709713277fad5420b44161e26a3d07be5d  /opt/nanobrew/cache/tmp/omp-17.1.6.tar.gz

On current main:

  • Formula has no field for the formula URL's using: strategy:
    pub const Formula = struct {
    name: []const u8,
    version: []const u8,
    revision: u32 = 0,
    rebuild: u32 = 0,
    desc: []const u8 = "",
    homepage: []const u8 = "",
    license: []const u8 = "",
    dependencies: []const []const u8 = &.{},
    bottle_url: []const u8 = "",
    bottle_sha256: []const u8 = "",
    source_url: []const u8 = "",
    source_sha256: []const u8 = "",
    build_deps: []const []const u8 = &.{},
    install_binaries: []const []const u8 = &.{},
    caveats: []const u8 = "",
    post_install_defined: bool = false,
  • The tap parser captures url and sha256, but not using: :nounzip:

    nanobrew/src/api/tap.zig

    Lines 461 to 480 in 308986a

    // --- Top-level fields ---
    if (version == null) {
    if (extractQuotedAfter(line, "version")) |val| {
    version = val;
    }
    }
    if (desc == null) {
    if (extractQuotedAfter(line, "desc")) |val| {
    desc = val;
    }
    }
    if (source_url == null) {
    if (extractQuotedAfter(line, "url")) |val| {
    source_url = val;
    }
    }
    if (source_sha256 == null and !in_bottle) {
    if (extractQuotedAfter(line, "sha256")) |val| {
    source_sha256 = val;
    }
  • A URL without a known archive suffix is renamed with the default .tar.gz suffix:
    fn archiveSuffixFromUrl(url: []const u8) []const u8 {
    const path = blk: {
    if (std.mem.indexOfScalar(u8, url, '?')) |q| break :blk url[0..q];
    break :blk url;
    };
    const suffixes: []const []const u8 = &.{
    ".tar.xz", ".tar.bz2", ".tar.gz", ".txz", ".tbz2", ".tgz", ".zip",
    };
    for (suffixes) |s| {
    if (path.len >= s.len and std.mem.endsWith(u8, path, s)) return s;
    }
    return ".tar.gz";
    }
  • Every non-.zip source is then passed to tar xf:
    // 3. Extract
    var build_dir_buf: [512]u8 = undefined;
    const build_dir = std.fmt.bufPrint(&build_dir_buf, "{s}/{s}-{s}-build", .{
    CACHE_TMP, formula.name, formula.version,
    }) catch return error.PathTooLong;
    printOut(lib_io, "==> Extracting source...\n", .{});
    // Clean previous build dir
    std.Io.Dir.cwd().deleteTree(lib_io, build_dir) catch {};
    std.Io.Dir.createDirAbsolute(lib_io, build_dir, .default_dir) catch {};
    {
    const argv: []const []const u8 = if (std.mem.endsWith(u8, tarball_path, ".zip"))
    &.{ "unzip", "-q", tarball_path, "-d", build_dir }
    else
    &.{ "tar", "xf", tarball_path, "-C", build_dir };
    runCommand(lib_io, .inherit, argv) catch |err| {
    printErr(lib_io, "nb: extract command failed: {}\n", .{err});
    return error.ExtractFailed;
    };

There is a second step needed for the end-to-end fix: this formula installs and renames the raw asset with:

bin.install Dir["omp-*"].first => "omp"

That non-literal install expression is currently treated as unparseable, which clears install_binaries:

nanobrew/src/api/tap.zig

Lines 329 to 353 in 308986a

// --- def install body: capture bin.install targets, skip everything else ---
if (in_install_method) {
if (endsWith(line, " do") or std.mem.eql(u8, line, "do")) {
install_inner_depth += 1;
continue;
}
if (std.mem.eql(u8, line, "end")) {
if (install_inner_depth > 0) {
install_inner_depth -= 1;
} else {
in_install_method = false;
}
continue;
}
if (isBinInstallLine(line)) {
switch (extractBinInstallNames(line)) {
.names => |names| for (names) |maybe_name| {
const bin_name = maybe_name orelse break;
try install_bins.append(alloc, try alloc.dupe(u8, bin_name));
},
.unparseable => bin_install_unparseable = true,
.none => {},
}
}
continue;

The generic fallback copies top-level files to the keg root, while the linker publishes executables from the keg's bin/ directory. Therefore, merely skipping tar would likely leave the executable unlinked.

Expected behavior

Nanobrew should:

  1. Preserve and honor using: :nounzip for tap formula URLs.
  2. Stage the raw download with its upstream basename (omp-darwin-arm64) instead of inventing a tar suffix.
  3. Apply the formula's install/rename semantics so the final keg contains executable bin/omp.
  4. Complete successfully and make omp available through the Nanobrew prefix.
  5. Return a nonzero status when installation fails.

An end-to-end regression test using this formula shape would catch both the extraction failure and the possible keg-root/unlinked follow-up failure.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions