Skip to content

Commit dd44869

Browse files
committed
fix(build): raise open-file limit for host musl cross-compile on macOS
Closes #2112 The host `cargo zigbuild` for `*-unknown-linux-musl` opens ~333 `.rlib` files at once during the static link, exceeding macOS's default soft limit of 256 and failing with `ProcessFdQuotaExceeded`. This blocked the docker/podman `mise run gateway` paths for macOS contributors; only the VM driver path guarded against it. Extract the VM path's `ensure_build_nofile_limit` guard into a shared `tasks/scripts/build-env.sh` and call it from the host-staging chokepoint (`stage-prebuilt-binaries.sh`), fixing docker, podman, and all docker:*/multiarch host cross-compiles at once. De-duplicate the VM script to source the shared helper. The guard is a no-op on Linux and when cargo-zigbuild is absent, so CI and Linux dev are unaffected. The limit is read from `OPENSHELL_BUILD_NOFILE_LIMIT` (default 8192), honoring the legacy `OPENSHELL_VM_BUILD_NOFILE_LIMIT` for back-compat. Also correct the stale comment in gateway-docker.sh (the cross-compile runs on the host, not inside Linux containers) and document the guard in architecture/build.md. Adds tasks/scripts/test-build-env.sh, wired into `mise run test` via `test:build-env`. Signed-off-by: Jim Meyer <jim@meyer4hire.com>
1 parent 3dee557 commit dd44869

7 files changed

Lines changed: 197 additions & 63 deletions

File tree

architecture/build.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,12 @@ and the supervisor image from `deploy/docker/Dockerfile.supervisor`. Neither
5959
Dockerfile compiles Rust — both copy a staged binary out of
6060
`deploy/docker/.build/prebuilt-binaries/<arch>/` into the final image.
6161

62-
Binary staging is driven by `tasks/scripts/stage-prebuilt-binaries.sh`. Gateway
62+
Binary staging is driven by `tasks/scripts/stage-prebuilt-binaries.sh`. Because
63+
staging cross-compiles on the host, it sources `tasks/scripts/build-env.sh` and
64+
raises the per-process open-file limit before invoking `cargo zigbuild` on
65+
macOS — the static musl link opens hundreds of `.rlib` files at once and would
66+
otherwise fail with `ProcessFdQuotaExceeded` under macOS's default soft limit of
67+
256. The guard is a no-op on Linux and when `cargo-zigbuild` is absent. Gateway
6368
binaries use `cargo zigbuild` with GNU targets pinned to glibc 2.28, including
6469
native-architecture builds, so the gateway image, standalone tarballs, and Linux
6570
packages share the same host portability floor. The gateway build enables

tasks/scripts/build-env.sh

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#!/usr/bin/env bash
2+
3+
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
4+
# SPDX-License-Identifier: Apache-2.0
5+
6+
# Shared build-environment helpers for host-side cross-compilation.
7+
#
8+
# Source this file (do not execute it) and call the helpers before invoking
9+
# cargo-zigbuild on the host.
10+
11+
# ensure_build_nofile_limit raises the per-process open-file limit before a
12+
# host cargo-zigbuild cross-compile. The static *-unknown-linux-musl link opens
13+
# hundreds of .rlib files simultaneously, which exceeds macOS's default soft
14+
# limit of 256 and fails with ProcessFdQuotaExceeded. The raised limit
15+
# propagates to the cargo/zig children the caller spawns.
16+
#
17+
# The limit is read from OPENSHELL_BUILD_NOFILE_LIMIT (default 8192), honoring
18+
# the legacy OPENSHELL_VM_BUILD_NOFILE_LIMIT for back-compat. This is a no-op on
19+
# Linux and when cargo-zigbuild is not installed (native builds, CI Linux
20+
# runners), so it must be safe to call unconditionally.
21+
ensure_build_nofile_limit() {
22+
local desired="${OPENSHELL_BUILD_NOFILE_LIMIT:-${OPENSHELL_VM_BUILD_NOFILE_LIMIT:-8192}}"
23+
local minimum=1024
24+
local current=""
25+
local hard=""
26+
local target=""
27+
28+
[ "$(uname -s)" = "Darwin" ] || return 0
29+
command -v cargo-zigbuild >/dev/null 2>&1 || return 0
30+
31+
current="$(ulimit -n 2>/dev/null || echo "")"
32+
case "${current}" in
33+
''|*[!0-9]*)
34+
return 0
35+
;;
36+
esac
37+
38+
if [ "${current}" -ge "${desired}" ]; then
39+
return 0
40+
fi
41+
42+
hard="$(ulimit -Hn 2>/dev/null || echo "")"
43+
target="${desired}"
44+
case "${hard}" in
45+
''|unlimited|infinity)
46+
;;
47+
*[!0-9]*)
48+
;;
49+
*)
50+
if [ "${hard}" -lt "${target}" ]; then
51+
target="${hard}"
52+
fi
53+
;;
54+
esac
55+
56+
if [ "${target}" -gt "${current}" ] && ulimit -n "${target}" 2>/dev/null; then
57+
echo "==> Raised open file limit for cargo-zigbuild: ${current} -> $(ulimit -n)"
58+
fi
59+
60+
current="$(ulimit -n 2>/dev/null || echo "${current}")"
61+
case "${current}" in
62+
''|*[!0-9]*)
63+
return 0
64+
;;
65+
esac
66+
67+
if [ "${current}" -lt "${desired}" ]; then
68+
echo "WARNING: Open file limit is ${current}; cargo-zigbuild is more reliable at ${desired}+ on macOS."
69+
fi
70+
71+
if [ "${current}" -lt "${minimum}" ]; then
72+
echo "ERROR: Open file limit (${current}) is too low for cargo-zigbuild on macOS." >&2
73+
echo " Run: ulimit -n ${desired}" >&2
74+
echo " Then re-run this script." >&2
75+
exit 1
76+
fi
77+
}

tasks/scripts/gateway-docker.sh

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,11 @@ HOST_OS="$(uname -s)"
112112
HOST_ARCH="$(normalize_arch "$(uname -m)")"
113113
SUPERVISOR_TARGET="$(linux_target_triple "${DAEMON_ARCH}")"
114114
# Cache the supervisor binary alongside the gateway state. Reuses the same
115-
# Docker pipeline used for the supervisor image, so the cross-compile happens
116-
# inside Linux containers — sidestepping macOS's per-process
117-
# file-descriptor cap that breaks zig/ld for this many rlibs.
115+
# Docker pipeline used for the supervisor image. The Dockerfiles do not compile
116+
# Rust; the cross-compile runs on the host via cargo zigbuild and the binary is
117+
# staged into the build context. On macOS the host staging path raises the
118+
# per-process file-descriptor limit automatically (see build-env.sh), so the
119+
# static musl link no longer hits ProcessFdQuotaExceeded on this many rlibs.
118120
SUPERVISOR_OUT_DIR="${STATE_DIR}/supervisor/${DAEMON_ARCH}"
119121
SUPERVISOR_BIN="${SUPERVISOR_OUT_DIR}/openshell-sandbox"
120122

tasks/scripts/stage-prebuilt-binaries.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ set -euo pipefail
88
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
99
ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
1010

11+
# shellcheck source=tasks/scripts/build-env.sh
12+
source "${SCRIPT_DIR}/build-env.sh"
13+
1114
usage() {
1215
echo "Usage: stage-prebuilt-binaries.sh <gateway|sandbox|supervisor|supervisor-output|all>" >&2
1316
}
@@ -230,6 +233,11 @@ fi
230233
restore_cargo_toml=0
231234
trap restore_workspace_version EXIT
232235

236+
# Raise the open-file limit before any host cargo-zigbuild cross-compile. This
237+
# single chokepoint covers the docker, podman, and all docker:*/multiarch host
238+
# staging paths. No-op on Linux and when cargo-zigbuild is absent.
239+
ensure_build_nofile_limit
240+
233241
patch_workspace_version
234242

235243
arches=()

tasks/scripts/test-build-env.sh

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
#!/usr/bin/env bash
2+
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
set -euo pipefail
6+
7+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8+
9+
# shellcheck source=tasks/scripts/build-env.sh
10+
source "${SCRIPT_DIR}/build-env.sh"
11+
12+
pass() { echo "PASS: $1"; }
13+
fail() {
14+
echo "FAIL: $1" >&2
15+
exit 1
16+
}
17+
18+
# The function must be defined by sourcing the helper.
19+
if ! declare -F ensure_build_nofile_limit >/dev/null; then
20+
fail "ensure_build_nofile_limit not defined after sourcing build-env.sh"
21+
fi
22+
pass "ensure_build_nofile_limit is defined"
23+
24+
os="$(uname -s)"
25+
26+
if [ "${os}" != "Darwin" ]; then
27+
# On non-Darwin hosts the helper must be a no-op regardless of the current
28+
# limit — CI Linux runners and native Linux dev must be unaffected.
29+
(
30+
ulimit -Sn 256 2>/dev/null || true
31+
before="$(ulimit -n)"
32+
ensure_build_nofile_limit >/dev/null
33+
after="$(ulimit -n)"
34+
[ "${before}" = "${after}" ] || fail "limit changed on non-Darwin host (${before} -> ${after})"
35+
)
36+
pass "no-op on non-Darwin host (${os})"
37+
echo "All build-env tests passed."
38+
exit 0
39+
fi
40+
41+
# Darwin below.
42+
if ! command -v cargo-zigbuild >/dev/null 2>&1; then
43+
# Without cargo-zigbuild the helper must be a no-op even on macOS.
44+
(
45+
ulimit -Sn 256 2>/dev/null || true
46+
before="$(ulimit -n)"
47+
ensure_build_nofile_limit >/dev/null
48+
after="$(ulimit -n)"
49+
[ "${before}" = "${after}" ] || fail "limit changed on macOS without cargo-zigbuild (${before} -> ${after})"
50+
)
51+
pass "no-op on macOS without cargo-zigbuild"
52+
echo "All build-env tests passed."
53+
exit 0
54+
fi
55+
56+
# Darwin + cargo-zigbuild: the helper should raise a low soft limit.
57+
(
58+
if ! ulimit -Sn 256 2>/dev/null; then
59+
echo "SKIP: unable to lower soft limit to 256 for test"
60+
exit 0
61+
fi
62+
ensure_build_nofile_limit >/dev/null
63+
after="$(ulimit -n)"
64+
hard="$(ulimit -Hn 2>/dev/null || echo unlimited)"
65+
case "${hard}" in
66+
unlimited|infinity)
67+
[ "${after}" -ge 8192 ] || fail "expected soft limit >= 8192, got ${after}"
68+
;;
69+
*[!0-9]*)
70+
[ "${after}" -gt 256 ] || fail "expected soft limit to rise above 256, got ${after}"
71+
;;
72+
*)
73+
expected=8192
74+
[ "${hard}" -lt "${expected}" ] && expected="${hard}"
75+
[ "${after}" -ge "${expected}" ] || fail "expected soft limit >= ${expected}, got ${after}"
76+
;;
77+
esac
78+
)
79+
pass "raises low soft limit on macOS with cargo-zigbuild"
80+
81+
# Idempotent: when the current limit already meets the desired value, the helper
82+
# leaves it unchanged (drive this via the env override so it holds regardless of
83+
# the host default).
84+
(
85+
before="$(ulimit -n)"
86+
OPENSHELL_BUILD_NOFILE_LIMIT=1 ensure_build_nofile_limit >/dev/null
87+
after="$(ulimit -n)"
88+
[ "${before}" = "${after}" ] || fail "limit changed when already above desired (${before} -> ${after})"
89+
)
90+
pass "no-op when current limit already meets desired"
91+
92+
echo "All build-env tests passed."

tasks/scripts/vm/build-supervisor-bundle.sh

Lines changed: 3 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ set -euo pipefail
77
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
88
OUTPUT_DIR="${OPENSHELL_VM_RUNTIME_COMPRESSED_DIR:-${ROOT}/target/vm-runtime-compressed}"
99

10+
# shellcheck source=tasks/scripts/build-env.sh
11+
source "${ROOT}/tasks/scripts/build-env.sh"
12+
1013
GUEST_ARCH=""
1114
while [[ $# -gt 0 ]]; do
1215
case "$1" in
@@ -58,64 +61,6 @@ esac
5861
SUPERVISOR_BIN="${ROOT}/target/${RUST_TARGET}/release/openshell-sandbox"
5962
SUPERVISOR_OUTPUT="${OUTPUT_DIR}/openshell-sandbox.zst"
6063

61-
ensure_build_nofile_limit() {
62-
local desired="${OPENSHELL_VM_BUILD_NOFILE_LIMIT:-8192}"
63-
local minimum=1024
64-
local current=""
65-
local hard=""
66-
local target=""
67-
68-
[ "$(uname -s)" = "Darwin" ] || return 0
69-
command -v cargo-zigbuild >/dev/null 2>&1 || return 0
70-
71-
current="$(ulimit -n 2>/dev/null || echo "")"
72-
case "${current}" in
73-
''|*[!0-9]*)
74-
return 0
75-
;;
76-
esac
77-
78-
if [ "${current}" -ge "${desired}" ]; then
79-
return 0
80-
fi
81-
82-
hard="$(ulimit -Hn 2>/dev/null || echo "")"
83-
target="${desired}"
84-
case "${hard}" in
85-
''|unlimited|infinity)
86-
;;
87-
*[!0-9]*)
88-
;;
89-
*)
90-
if [ "${hard}" -lt "${target}" ]; then
91-
target="${hard}"
92-
fi
93-
;;
94-
esac
95-
96-
if [ "${target}" -gt "${current}" ] && ulimit -n "${target}" 2>/dev/null; then
97-
echo "==> Raised open file limit for cargo-zigbuild: ${current} -> $(ulimit -n)"
98-
fi
99-
100-
current="$(ulimit -n 2>/dev/null || echo "${current}")"
101-
case "${current}" in
102-
''|*[!0-9]*)
103-
return 0
104-
;;
105-
esac
106-
107-
if [ "${current}" -lt "${desired}" ]; then
108-
echo "WARNING: Open file limit is ${current}; cargo-zigbuild is more reliable at ${desired}+ on macOS."
109-
fi
110-
111-
if [ "${current}" -lt "${minimum}" ]; then
112-
echo "ERROR: Open file limit (${current}) is too low for cargo-zigbuild on macOS." >&2
113-
echo " Run: ulimit -n ${desired}" >&2
114-
echo " Then re-run this script." >&2
115-
exit 1
116-
fi
117-
}
118-
11964
echo "==> Building openshell-sandbox supervisor bundle"
12065
echo " Guest arch: ${GUEST_ARCH}"
12166
echo " Rust target: ${RUST_TARGET}"

tasks/test.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
[test]
77
description = "Run all tests (Rust + Python)"
8-
depends = ["test:rust", "test:python", "test:sbom", "test:install-sh", "test:packaging-assets", "test:docs-website"]
8+
depends = ["test:rust", "test:python", "test:sbom", "test:install-sh", "test:build-env", "test:packaging-assets", "test:docs-website"]
99

1010
["test:docs-website"]
1111
description = "Test the docs-website sync script"
@@ -23,6 +23,11 @@ description = "Run focused install.sh shell tests"
2323
run = "tasks/scripts/test-install-sh.sh"
2424
hide = true
2525

26+
["test:build-env"]
27+
description = "Run build-env.sh helper shell tests"
28+
run = "tasks/scripts/test-build-env.sh"
29+
hide = true
30+
2631
["test:packaging-assets"]
2732
description = "Run static packaging asset tests"
2833
run = "tasks/scripts/test-packaging-assets.sh"

0 commit comments

Comments
 (0)