From 4304e81aab182e11a543497039657a2a0b147d8a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 20 Apr 2026 20:44:05 -0700 Subject: [PATCH] add self-contained Homebrew bottle packages for Darwin Add `packages.brew.` exposing Zig builds from Homebrew official binary bottles. These bottles carry macOS-specific patches that the upstream ziglang.org tarballs do not include. Homebrew bottles are dynamically linked against Homebrew-provided LLVM, LLD, and zstd. The bottles ship with @@HOMEBREW_PREFIX@@ relocation markers in their Mach-O load commands instead of real paths, expecting Homebrew to rewrite them at install time. To make the derivations fully self-contained without requiring a Homebrew installation at runtime, the builder downloads the dependency bottles too, then walks the dylib closure starting from the zig binary using otool -L. Each referenced non-system dylib is copied into $out/lib/brew/, its install name is rewritten to @rpath/, and the zig binary gets an rpath of @executable_path/../lib/brew. Bundled dylibs get @loader_path so they find each other. Every patched Mach-O is re-signed with an ad-hoc signature since install_name_tool invalidates the original code signature, and macOS kills unsigned arm64 binaries. --- README.md | 42 ++++++++++-- brew-sources.json | 164 ++++++++++++++++++++++++++++++++++++++++++++++ default.nix | 162 ++++++++++++++++++++++++++++++++++++++++++++- update-brew | 127 +++++++++++++++++++++++++++++++++++ 4 files changed, 487 insertions(+), 8 deletions(-) create mode 100644 brew-sources.json create mode 100755 update-brew diff --git a/README.md b/README.md index 3e006b1f..37913bb3 100644 --- a/README.md +++ b/README.md @@ -9,13 +9,14 @@ This repository is meant to be consumed primarily as a flake but the The flake outputs are documented in `flake.nix` but an overview: - * Default package and "app" is the latest released version - * `packages.` for a tagged release - * `packages.master` for the latest nightly release - * `packages.master-` for a nightly release - * `overlays.default` is an overlay that adds `zigpkgs` to be the packages +* Default package and "app" is the latest released version +* `packages.` for a tagged release +* `packages.master` for the latest nightly release +* `packages.master-` for a nightly release +* `packages.brew.` for a Homebrew bottle build (macOS only) +* `overlays.default` is an overlay that adds `zigpkgs` to be the packages exposed by this flake - * `templates.compiler-dev` to setup a development environment for Zig +* `templates.compiler-dev` to setup a development environment for Zig compiler development. ## Usage @@ -47,6 +48,33 @@ $ nix shell 'github:mitchellh/zig-overlay#master' $ nix shell 'github:mitchellh/zig-overlay#"0.14.0"' ``` +### Homebrew Bottles + +The `brew` package set provides Zig builds from Homebrew's official +binary bottles. These are built from source by Homebrew with +macOS-specific patches applied. The derivations are fully +self-contained — all dylib dependencies (LLVM, LLD, zstd) are +bundled and patched so no Homebrew installation is required at +runtime. + +These can be useful because in some situations Homebrew backports +more patches to their bottles than the official Zig releases to keep +compatibility with newer macOS versions. + +Brew packages are only available on macOS (`aarch64-darwin` and +`x86_64-darwin`). + +```sh +# use the Homebrew-built Zig 0.16.0 +$ nix shell 'github:mitchellh/zig-overlay#brew."0.16.0"' +``` + +Via the overlay: + +```nix +zigpkgs.brew."0.16.0" +``` + ### Compiler Development This flake outputs a template that makes it easy to work on the Zig @@ -80,7 +108,7 @@ we could miss a day. This is why historical dates beyond a certain point don't exist; they predate this overlay (or original overlays this derives from). -2. The official Zig CI only generates a master release if the CI runs +2. The official Zig CI only generates a master release if the CI runs full green. During certain periods of development, a full day may go by where the master branch of the Zig compiler is broken. In this scenario, a master build (aka "nightly") is not built or released at all. diff --git a/brew-sources.json b/brew-sources.json new file mode 100644 index 00000000..0487dccb --- /dev/null +++ b/brew-sources.json @@ -0,0 +1,164 @@ +{ + "0.16.0": { + "aarch64-darwin": { + "version": "0.16.0", + "formula": "zig", + "bottles": { + "zig": { + "url": "https://ghcr.io/v2/homebrew/core/zig/blobs/sha256:8f023cdd84a92e2fff016a77fba8f46bbf6edbe8cbc86315ee682243ed882c5d", + "sha256": "8f023cdd84a92e2fff016a77fba8f46bbf6edbe8cbc86315ee682243ed882c5d", + "root": "zig/0.16.0" + }, + "lld@21": { + "url": "https://ghcr.io/v2/homebrew/core/lld/21/blobs/sha256:661c8726f564040c92e7711eb3cf2349bae47d2b61fb685d9660226541fe3a26", + "sha256": "661c8726f564040c92e7711eb3cf2349bae47d2b61fb685d9660226541fe3a26", + "root": "lld@21/21.1.8_1" + }, + "llvm@21": { + "url": "https://ghcr.io/v2/homebrew/core/llvm/21/blobs/sha256:1d8422fcaacee615ff78ba5ac530b8a141ce127087b3e861ba1faf972dc88256", + "sha256": "1d8422fcaacee615ff78ba5ac530b8a141ce127087b3e861ba1faf972dc88256", + "root": "llvm@21/21.1.8" + }, + "zstd": { + "url": "https://ghcr.io/v2/homebrew/core/zstd/blobs/sha256:35b5150b27512a94ebaee7b4399aaa8adf42d247e6968319e4aeac3c05365281", + "sha256": "35b5150b27512a94ebaee7b4399aaa8adf42d247e6968319e4aeac3c05365281", + "root": "zstd/1.5.7_1" + } + } + }, + "x86_64-darwin": { + "version": "0.16.0", + "formula": "zig", + "bottles": { + "zig": { + "url": "https://ghcr.io/v2/homebrew/core/zig/blobs/sha256:4a0c5f8df1c9bf09ca878d48d331024be0f9825dedfd7ab384cba59c8bd635c4", + "sha256": "4a0c5f8df1c9bf09ca878d48d331024be0f9825dedfd7ab384cba59c8bd635c4", + "root": "zig/0.16.0" + }, + "lld@21": { + "url": "https://ghcr.io/v2/homebrew/core/lld/21/blobs/sha256:925d4df8fea1bf6e676095415ce8165ac5e539cd8bb1f3b143f3b480780c76b6", + "sha256": "925d4df8fea1bf6e676095415ce8165ac5e539cd8bb1f3b143f3b480780c76b6", + "root": "lld@21/21.1.8_1" + }, + "llvm@21": { + "url": "https://ghcr.io/v2/homebrew/core/llvm/21/blobs/sha256:e58f968196af2f0db01f546cae78792d0b0cf885641c380ec3efa10817f52d13", + "sha256": "e58f968196af2f0db01f546cae78792d0b0cf885641c380ec3efa10817f52d13", + "root": "llvm@21/21.1.8" + }, + "zstd": { + "url": "https://ghcr.io/v2/homebrew/core/zstd/blobs/sha256:8b8656acd6f30bcbbb9a033ae840afea299c9f0852f71b7540492b0fe7a36742", + "sha256": "8b8656acd6f30bcbbb9a033ae840afea299c9f0852f71b7540492b0fe7a36742", + "root": "zstd/1.5.7_1" + } + } + } + }, + "0.15.2": { + "aarch64-darwin": { + "version": "0.15.2", + "formula": "zig@0.15", + "bottles": { + "zig@0.15": { + "url": "https://ghcr.io/v2/homebrew/core/zig/0.15/blobs/sha256:50e79a382a4310c562a5afbdc15435018a5e4d9ff9149729261670e6eaced4b6", + "sha256": "50e79a382a4310c562a5afbdc15435018a5e4d9ff9149729261670e6eaced4b6", + "root": "zig@0.15/0.15.2" + }, + "lld@20": { + "url": "https://ghcr.io/v2/homebrew/core/lld/20/blobs/sha256:765b919096c9718ef2dd15103c7f9989956550dd0f4eb356dbfcda65ff96feb3", + "sha256": "765b919096c9718ef2dd15103c7f9989956550dd0f4eb356dbfcda65ff96feb3", + "root": "lld@20/20.1.8" + }, + "llvm@20": { + "url": "https://ghcr.io/v2/homebrew/core/llvm/20/blobs/sha256:1fefd983fc606f81c1715af4dbecfa8fbe2967196e06bcac6d63baad878c511d", + "sha256": "1fefd983fc606f81c1715af4dbecfa8fbe2967196e06bcac6d63baad878c511d", + "root": "llvm@20/20.1.8" + }, + "zstd": { + "url": "https://ghcr.io/v2/homebrew/core/zstd/blobs/sha256:35b5150b27512a94ebaee7b4399aaa8adf42d247e6968319e4aeac3c05365281", + "sha256": "35b5150b27512a94ebaee7b4399aaa8adf42d247e6968319e4aeac3c05365281", + "root": "zstd/1.5.7_1" + } + } + }, + "x86_64-darwin": { + "version": "0.15.2", + "formula": "zig@0.15", + "bottles": { + "zig@0.15": { + "url": "https://ghcr.io/v2/homebrew/core/zig/0.15/blobs/sha256:fbf88162ef3557bbf834f9d2ca329e12ca3d0d383f52e073f48a61a63b650961", + "sha256": "fbf88162ef3557bbf834f9d2ca329e12ca3d0d383f52e073f48a61a63b650961", + "root": "zig@0.15/0.15.2" + }, + "lld@20": { + "url": "https://ghcr.io/v2/homebrew/core/lld/20/blobs/sha256:d4a01c675382bd918542b3a035a8731dd338c94cf1d67769fa2aa5cc84ef4a15", + "sha256": "d4a01c675382bd918542b3a035a8731dd338c94cf1d67769fa2aa5cc84ef4a15", + "root": "lld@20/20.1.8" + }, + "llvm@20": { + "url": "https://ghcr.io/v2/homebrew/core/llvm/20/blobs/sha256:62c1cbffed8c724ef4201147e6bc80830e25456f465b09ec904731256741be09", + "sha256": "62c1cbffed8c724ef4201147e6bc80830e25456f465b09ec904731256741be09", + "root": "llvm@20/20.1.8" + }, + "zstd": { + "url": "https://ghcr.io/v2/homebrew/core/zstd/blobs/sha256:8b8656acd6f30bcbbb9a033ae840afea299c9f0852f71b7540492b0fe7a36742", + "sha256": "8b8656acd6f30bcbbb9a033ae840afea299c9f0852f71b7540492b0fe7a36742", + "root": "zstd/1.5.7_1" + } + } + } + }, + "0.14.1": { + "aarch64-darwin": { + "version": "0.14.1", + "formula": "zig@0.14", + "bottles": { + "zig@0.14": { + "url": "https://ghcr.io/v2/homebrew/core/zig/0.14/blobs/sha256:bf2bb8d09f1eb6c311609f6d5699206199e06d4d60924e1d19c9e72ba0537b6a", + "sha256": "bf2bb8d09f1eb6c311609f6d5699206199e06d4d60924e1d19c9e72ba0537b6a", + "root": "zig@0.14/0.14.1" + }, + "lld@19": { + "url": "https://ghcr.io/v2/homebrew/core/lld/19/blobs/sha256:58080e6765d2bb2177de344b622f7084a1d0897a6925888a561cac99f34113f5", + "sha256": "58080e6765d2bb2177de344b622f7084a1d0897a6925888a561cac99f34113f5", + "root": "lld@19/19.1.7" + }, + "llvm@19": { + "url": "https://ghcr.io/v2/homebrew/core/llvm/19/blobs/sha256:37747f7b66d323ef07f18d471b928480a67ad93de8d5f7bdd3418367f1a8eac6", + "sha256": "37747f7b66d323ef07f18d471b928480a67ad93de8d5f7bdd3418367f1a8eac6", + "root": "llvm@19/19.1.7" + }, + "zstd": { + "url": "https://ghcr.io/v2/homebrew/core/zstd/blobs/sha256:35b5150b27512a94ebaee7b4399aaa8adf42d247e6968319e4aeac3c05365281", + "sha256": "35b5150b27512a94ebaee7b4399aaa8adf42d247e6968319e4aeac3c05365281", + "root": "zstd/1.5.7_1" + } + } + }, + "x86_64-darwin": { + "version": "0.14.1", + "formula": "zig@0.14", + "bottles": { + "zig@0.14": { + "url": "https://ghcr.io/v2/homebrew/core/zig/0.14/blobs/sha256:c7ac8f1ea8b09c4c9706597da2eef4317c56f46801be7fb699b11fde357dfb1d", + "sha256": "c7ac8f1ea8b09c4c9706597da2eef4317c56f46801be7fb699b11fde357dfb1d", + "root": "zig@0.14/0.14.1" + }, + "lld@19": { + "url": "https://ghcr.io/v2/homebrew/core/lld/19/blobs/sha256:363576465702b57491a005d7ba9a12aa03b147810bfd8ef3ae563d608427a00d", + "sha256": "363576465702b57491a005d7ba9a12aa03b147810bfd8ef3ae563d608427a00d", + "root": "lld@19/19.1.7" + }, + "llvm@19": { + "url": "https://ghcr.io/v2/homebrew/core/llvm/19/blobs/sha256:7739d54a784eafaa3c7d76a112bbafdd16214e60a58e60bc9a41240d2b981577", + "sha256": "7739d54a784eafaa3c7d76a112bbafdd16214e60a58e60bc9a41240d2b981577", + "root": "llvm@19/19.1.7" + }, + "zstd": { + "url": "https://ghcr.io/v2/homebrew/core/zstd/blobs/sha256:8b8656acd6f30bcbbb9a033ae840afea299c9f0852f71b7540492b0fe7a36742", + "sha256": "8b8656acd6f30bcbbb9a033ae840afea299c9f0852f71b7540492b0fe7a36742", + "root": "zstd/1.5.7_1" + } + } + } + } +} diff --git a/default.nix b/default.nix index 7af39072..a0821030 100644 --- a/default.nix +++ b/default.nix @@ -6,6 +6,7 @@ inherit (pkgs) lib; sources = builtins.fromJSON (lib.strings.fileContents ./sources.json); mirrors = builtins.fromJSON (lib.strings.fileContents ./mirrors.json); + brewSources = builtins.fromJSON (lib.strings.fileContents ./brew-sources.json); # mkBinaryInstall makes a derivation that installs Zig from a binary. mkBinaryInstall = { @@ -71,6 +72,154 @@ }; }); + # mkBrewInstall makes a self-contained derivation that installs Zig from + # Homebrew bottles, bundling all dylib dependencies (LLVM, LLD, zstd). + # All @@HOMEBREW_PREFIX@@ and @rpath references are resolved so that no + # Homebrew installation is needed at runtime. + mkBrewInstall = { + version, + formula, + bottles, + }: let + # Fetch each bottle as a separate fixed-output derivation + bottleSrcs = + lib.attrsets.mapAttrs + (name: b: + pkgs.fetchurl { + inherit (b) url sha256; + curlOptsList = ["-H" "Authorization: Bearer QQ=="]; + }) + bottles; + in + pkgs.stdenv.mkDerivation (finalAttrs: { + inherit version; + + pname = "zig"; + dontConfigure = true; + dontBuild = true; + dontFixup = true; + dontUnpack = true; + nativeBuildInputs = [pkgs.darwin.cctools pkgs.darwin.sigtool]; + + installPhase = let + # Unpack all bottles into $TMPDIR/bottles + unpackCmds = lib.concatStringsSep "\n" ( + lib.attrsets.mapAttrsToList + (name: src: "tar -xzf ${src} -C $TMPDIR/bottles") + bottleSrcs + ); + zigRoot = bottles.${formula}.root; + in '' + mkdir -p $TMPDIR/bottles + ${unpackCmds} + + # Install zig + mkdir -p $out/{bin,lib,lib/brew} + cp $TMPDIR/bottles/${zigRoot}/bin/zig $out/bin/zig + cp -r $TMPDIR/bottles/${zigRoot}/lib/* $out/lib + substituteInPlace $out/lib/zig/std/zig/system.zig \ + --replace-fail "/usr/bin/env" "${pkgs.lib.getExe' pkgs.coreutils "env"}" + + # Collect all candidate dylibs from dependency bottles + candidates=$TMPDIR/candidates + mkdir -p $candidates + find $TMPDIR/bottles -name '*.dylib' -exec cp -n {} $candidates/ \; + + # Recursively copy needed dylibs starting from the zig binary + copy_needed() { + local file="$1" + otool -L "$file" | tail -n +2 | awk '{print $1}' | while read -r dep; do + case "$dep" in + /usr/lib/*|/System/Library/*) continue ;; + esac + local base + base="$(basename "$dep")" + local target="$out/lib/brew/$base" + if [ ! -e "$target" ] && [ -e "$candidates/$base" ]; then + cp -L "$candidates/$base" "$target" + chmod u+w "$target" + copy_needed "$target" + fi + done + } + copy_needed $out/bin/zig + + # Patch all Mach-O files: rewrite dylib refs to @rpath + patch_file() { + local file="$1" + local is_zig_bin=0 + [ "$file" = "$out/bin/zig" ] && is_zig_bin=1 + + # Set dylib ID for bundled libs + if [ "$is_zig_bin" -eq 0 ]; then + install_name_tool -id "@rpath/$(basename "$file")" "$file" + fi + + # Rewrite all non-system deps to @rpath + otool -L "$file" | tail -n +2 | awk '{print $1}' | while read -r dep; do + case "$dep" in + /usr/lib/*|/System/Library/*|@rpath/*) continue ;; + esac + install_name_tool -change "$dep" "@rpath/$(basename "$dep")" "$file" + done + + # Add rpath + if [ "$is_zig_bin" -eq 1 ]; then + install_name_tool -add_rpath "@executable_path/../lib/brew" "$file" 2>/dev/null || true + else + install_name_tool -add_rpath "@loader_path" "$file" 2>/dev/null || true + fi + } + + # Patch zig binary and all bundled dylibs + patch_file $out/bin/zig + for dylib in $out/lib/brew/*.dylib; do + [ -e "$dylib" ] && patch_file "$dylib" + done + + # Re-sign all patched Mach-O files (required on macOS) + codesign -f -s - $out/bin/zig + for dylib in $out/lib/brew/*.dylib; do + [ -e "$dylib" ] && codesign -f -s - "$dylib" + done + + # Verify no Homebrew references remain + if otool -L $out/bin/zig $out/lib/brew/*.dylib 2>/dev/null | grep -q '@@HOMEBREW_PREFIX@@\|/opt/homebrew\|/usr/local/opt'; then + echo "ERROR: Homebrew references still present:" >&2 + otool -L $out/bin/zig $out/lib/brew/*.dylib | grep '@@HOMEBREW_PREFIX@@\|/opt/homebrew\|/usr/local/opt' >&2 + exit 1 + fi + ''; + + passthru = let + mkPassthru = import "${nixpkgs}/pkgs/development/compilers/zig/passthru.nix"; + in + mkPassthru ({ + inherit + (pkgs) + stdenv + callPackage + wrapCCWith + wrapBintoolsWith + overrideCC + ; + zig = finalAttrs.finalPackage; + } + // lib.optionalAttrs ((builtins.functionArgs mkPassthru) ? lib) { + inherit lib; + } + // lib.optionalAttrs ((builtins.functionArgs mkPassthru) ? targetPackages) { + inherit (pkgs) targetPackages; + }); + + meta = + pkgs.zig.meta + // { + mainProgram = "zig"; + platforms = [system]; + }; + }); + # The packages that are tagged releases taggedPackages = lib.attrsets.mapAttrs @@ -100,6 +249,17 @@ (k: v: (builtins.hasAttr system v) && (v.${system}.url != null) && (v.${system}.sha256 != null)) sources.master); + # The packages from Homebrew bottles (macOS-patched builds) + brewPackages = + lib.attrsets.mapAttrs + (k: v: + mkBrewInstall { + inherit (v.${system}) version formula bottles; + }) + (lib.attrsets.filterAttrs + (k: v: (builtins.hasAttr system v) && (v.${system} ? bottles)) + brewSources); + # This determines the latest /released/ version. latest = lib.lists.last ( builtins.sort @@ -109,4 +269,4 @@ in # We want the packages but also add a "default" that just points to the # latest released version. - taggedPackages // masterPackages // {"default" = taggedPackages.${latest};} + taggedPackages // masterPackages // {"default" = taggedPackages.${latest};} // lib.optionalAttrs (brewPackages != {}) {brew = brewPackages;} diff --git a/update-brew b/update-brew new file mode 100755 index 00000000..ece7bcc4 --- /dev/null +++ b/update-brew @@ -0,0 +1,127 @@ +#!/usr/bin/env nix-shell +#! nix-shell -p coreutils curl jq -i sh +set -eu + +TMPDIR=$(mktemp -d) +trap "rm -rf '$TMPDIR'" EXIT + +fetch() { + local url="$1" dest="$2" + curl -sfL "$url" -o "$dest" +} + +BREW_API="https://formulae.brew.sh/api/formula" + +# ---------- Phase 1: Fetch all zig formula JSONs ---------- + +echo "Fetching zig formula..." >&2 +fetch "$BREW_API/zig.json" "$TMPDIR/zig.json" + +VERSIONED=$(jq -r '.versioned_formulae[]' "$TMPDIR/zig.json") +ZIG_FORMULAS="zig" + +for vf in $VERSIONED; do + echo "Fetching $vf formula..." >&2 + fetch "$BREW_API/$vf.json" "$TMPDIR/$vf.json" + ZIG_FORMULAS="$ZIG_FORMULAS $vf" +done + +# ---------- Phase 2: Recursively fetch dependency formulas ---------- + +FETCHED="$ZIG_FORMULAS" + +resolve_deps() { + local queue="$1" + while [ -n "$queue" ]; do + local next_queue="" + for formula in $queue; do + local deps + deps=$(jq -r '.dependencies[]' "$TMPDIR/$formula.json" 2>/dev/null || true) + for dep in $deps; do + # Check if already fetched + case " $FETCHED " in + *" $dep "*) continue ;; + esac + echo "Fetching dependency $dep..." >&2 + fetch "$BREW_API/$dep.json" "$TMPDIR/$dep.json" + FETCHED="$FETCHED $dep" + next_queue="$next_queue $dep" + done + done + queue="$(echo "$next_queue" | xargs)" + done +} + +resolve_deps "$ZIG_FORMULAS" + +echo "All formulas fetched: $FETCHED" >&2 + +# ---------- Phase 3: Assemble brew-sources.json ---------- + +# Build a single JSON database from all fetched formula files +DB="$TMPDIR/db.json" +jq -s 'map({(.name): .}) | add' "$TMPDIR"/*.json > "$DB" + +# Pass zig formula names as a JSON array +ZIG_NAMES_JSON=$(printf '%s\n' $ZIG_FORMULAS | jq -R . | jq -s .) + +echo "Assembling brew-sources.json..." >&2 + +jq -n \ + --argjson db "$(cat "$DB")" \ + --argjson zig_formulas "$ZIG_NAMES_JSON" \ +' +def best_tag(tags; prefs): + (prefs | map(select(. as $p | tags | has($p))) | first) // null; + +def bottle_entry(formula_name; tag): + $db[formula_name] as $f | + $f.bottle.stable.files[tag] as $b | + $f.versions.stable as $ver | + ($f.revision // 0) as $rev | + (if $rev > 0 then "\($ver)_\($rev)" else $ver end) as $pkg_ver | + { + url: $b.url, + sha256: $b.sha256, + root: "\(formula_name)/\($pkg_ver)" + }; + +def platform_entry(zig_name; nix_plat; prefs): + $db[zig_name] as $zf | + $zf.bottle.stable.files as $files | + best_tag($files; prefs) as $zig_tag | + if $zig_tag == null then null + else + ($zf.dependencies // []) as $deps | + ([$deps[] | . as $dep | + $db[$dep].bottle.stable.files as $df | + best_tag($df; prefs) as $dep_tag | + if $dep_tag == null then empty + else {($dep): bottle_entry($dep; $dep_tag)} + end + ] | add // {}) as $dep_bottles | + { + version: $zf.versions.stable, + formula: zig_name, + bottles: ({(zig_name): bottle_entry(zig_name; $zig_tag)} + $dep_bottles) + } + end; + +[ + $zig_formulas[] | . as $zf | + $db[$zf].versions.stable as $ver | + { + ($ver): ( + {} + + (platform_entry($zf; "aarch64-darwin"; + ["arm64_ventura","arm64_sonoma","arm64_sequoia","arm64_tahoe"]) + | if . then {"aarch64-darwin": .} else {} end) + + (platform_entry($zf; "x86_64-darwin"; + ["ventura","sonoma","sequoia","tahoe"]) + | if . then {"x86_64-darwin": .} else {} end) + ) + } +] | add +' > brew-sources.json + +echo "Wrote brew-sources.json" >&2