#!/usr/bin/env bash set -euo pipefail repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) cd "$repo_root" # shellcheck source=scripts/remote_config.sh source "$repo_root/scripts/remote_config.sh" jcode_load_remote_config log() { printf 'dev_cargo: %s\n' "$*" >&2 } selected_linker_mode="not-configured" selected_linker_desc="" sccache_status="disabled" selfdev_low_memory_status="disabled" feature_profile_status="default" build_jobs_status="cargo-default" git_meta_status="not-configured" append_rustflags() { local new_flag="$1" if [[ -z "${CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS:-}" ]]; then export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS="$new_flag" else export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS="${CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS} ${new_flag}" fi } selected_profile() { # Print the Cargo profile selected by the args, defaulting to "dev" (cargo's # default for build/check) when no --profile/--release is present. local expect_profile_name="false" local profile="dev" for arg in "$@"; do if [[ "$expect_profile_name" == "true" ]]; then profile="$arg" expect_profile_name="false" continue fi case "$arg" in --release|-r) profile="release" ;; --profile=*) profile="${arg#--profile=}" ;; --profile) expect_profile_name="true" ;; esac done printf '%s\n' "$profile" } # Determine whether the effective build will use incremental compilation. # sccache cannot cache incremental units, so this gates whether sccache is # useful at all. CARGO_INCREMENTAL (if set) wins; otherwise infer from the # profile's incremental setting in Cargo.toml. build_is_incremental() { case "${CARGO_INCREMENTAL:-}" in 0|false|no|off) return 1 ;; 1|true|yes|on) return 0 ;; esac case "$(selected_profile "$@")" in # Non-incremental profiles (see Cargo.toml): sccache can produce hits here. release-lto) return 1 ;; # selfdev/dev/release/test and unknown profiles default to incremental. *) return 0 ;; esac } maybe_enable_sccache() { case "${SCCACHE_DISABLE:-}" in 1|true|yes|on) if [[ -n "${RUSTC_WRAPPER:-}" && "${RUSTC_WRAPPER}" == *sccache* ]]; then unset RUSTC_WRAPPER fi sccache_status="disabled-by-env" log "sccache disabled by SCCACHE_DISABLE" return ;; esac if [[ -n "${RUSTC_WRAPPER:-}" ]]; then sccache_status="external:${RUSTC_WRAPPER}" log "keeping existing RUSTC_WRAPPER=${RUSTC_WRAPPER}" return fi # sccache cannot cache incremental compilations, so for our default # incremental profiles it produces 0% hits while adding wrapper overhead and # misleading "enabled" status. Skip it for incremental builds unless the # caller explicitly forces it via JCODE_SCCACHE=1/on/force. local force_sccache="${JCODE_SCCACHE:-auto}" case "$force_sccache" in 1|true|yes|on|force) force_sccache="1" ;; 0|false|no|off|never) sccache_status="disabled-by-jcode-sccache" log "sccache disabled by JCODE_SCCACHE" return ;; *) force_sccache="auto" ;; esac if [[ "$force_sccache" != "1" ]] && build_is_incremental "$@"; then sccache_status="skipped-incremental" log "sccache skipped for incremental build (it cannot cache incremental units; set JCODE_SCCACHE=on to force, or use a non-incremental profile)" return fi if command -v sccache >/dev/null 2>&1; then sccache --start-server >/dev/null 2>&1 || true export RUSTC_WRAPPER=sccache sccache_status="enabled" log "using sccache" else sccache_status="not-found" log "sccache not found; using direct rustc" fi } uses_selfdev_profile() { local expect_profile_name="false" for arg in "$@"; do if [[ "$expect_profile_name" == "true" ]]; then [[ "$arg" == "selfdev" ]] && return 0 expect_profile_name="false" continue fi case "$arg" in --profile=selfdev) return 0 ;; --profile) expect_profile_name="true" ;; esac done return 1 } has_explicit_feature_args() { local expect_value="false" for arg in "$@"; do if [[ "$expect_value" == "true" ]]; then expect_value="false" continue fi case "$arg" in --) return 1 ;; --features|--no-default-features) return 0 ;; --features=*|--no-default-features=*) return 0 ;; esac done return 1 } feature_args_from_profile() { local profile="$1" case "$profile" in ""|default) return 0 ;; minimal|none) printf '%s\0' --no-default-features ;; pdf) printf '%s\0' --no-default-features --features pdf ;; embeddings) printf '%s\0' --no-default-features --features embeddings ;; full) printf '%s\0' --features embeddings,pdf,bedrock ;; *) return 1 ;; esac } validate_feature_profile() { local profile="${JCODE_DEV_FEATURE_PROFILE:-default}" case "$profile" in ""|default|minimal|none|pdf|embeddings|full) ;; *) printf 'error: unsupported JCODE_DEV_FEATURE_PROFILE=%s (expected default|minimal|pdf|embeddings|full)\n' "$profile" >&2 exit 1 ;; esac } build_cargo_argv() { local profile="${JCODE_DEV_FEATURE_PROFILE:-default}" if [[ "$profile" == "default" || -z "$profile" ]]; then feature_profile_status="default" printf '%s\0' "$@" return 0 fi if has_explicit_feature_args "$@"; then feature_profile_status="ignored-explicit-cargo-args" printf '%s\0' "$@" return 0 fi local -a feature_args=() while IFS= read -r -d '' arg; do feature_args+=("$arg") done < <(feature_args_from_profile "$profile") feature_profile_status="$profile" local inserted="false" for arg in "$@"; do if [[ "$arg" == "--" && "$inserted" == "false" ]]; then printf '%s\0' "${feature_args[@]}" inserted="true" fi printf '%s\0' "$arg" done if [[ "$inserted" == "false" ]]; then printf '%s\0' "${feature_args[@]}" fi } meminfo_kib() { local key="$1" awk -v key="$key" '$1 == key ":" { print $2; exit }' /proc/meminfo 2>/dev/null || true } selfdev_low_memory_default_needed() { [[ "$(uname -s)" == "Linux" ]] || return 1 [[ -r /proc/meminfo ]] || return 1 command -v pgrep >/dev/null 2>&1 || return 1 pgrep -x earlyoom >/dev/null 2>&1 || return 1 local mem_total_kib mem_available_kib swap_total_kib mem_total_kib=$(meminfo_kib MemTotal) mem_available_kib=$(meminfo_kib MemAvailable) swap_total_kib=$(meminfo_kib SwapTotal) [[ -n "$mem_total_kib" && -n "$mem_available_kib" && -n "$swap_total_kib" ]] || return 1 # On small no-swap machines, earlyoom can terminate the root jcode rustc # around 1-3 GiB RSS before the kernel OOM killer would report anything. # Keep this adaptive so larger workstations, and currently-idle smaller # workstations with enough headroom, retain the faster inherited selfdev # profile by default. (( swap_total_kib == 0 && mem_total_kib < 24576 * 1024 && mem_available_kib < 8192 * 1024 )) } cpu_count() { local n n=$(nproc 2>/dev/null || getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1) [[ "$n" =~ ^[0-9]+$ && "$n" -ge 1 ]] || n=1 printf '%s\n' "$n" } # Choose a Cargo job count from *currently available* memory so concurrent # builds (e.g. several self-dev agents on one machine) self-throttle instead of # all assuming the full core count and tripping earlyoom/OOM. # # After the monolith was split into the jcode-base/app-core/tui/cli crate DAG, # the largest single rustc unit is jcode-base, which has grown back to a # measured ~1.6 GiB RSS peak (selfdev profile, sampled VmRSS while building the # lib), down from the old 2.5-3 GiB monolith but above the original ~1.28 GiB # post-split figure. We budget ~1.75 GiB of currently-available memory per job: # a deliberate cushion above the measured peak (rustc's true VmHWM can exceed a # coarse sample, and jcode-base keeps growing) so a fresh build under load backs # off before earlyoom SIGTERMs it. Clamp into [1, cpus]. On an idle 15 GiB # machine this still uses ~7 of 8 cores; under memory pressure a fresh build # backs off further. An explicit CARGO_BUILD_JOBS / JCODE_BUILD_JOBS always # wins, and non-Linux hosts fall back to the cargo/.cargo default. select_build_jobs() { # Respect an explicit override from either env var. local override="${JCODE_BUILD_JOBS:-${CARGO_BUILD_JOBS:-}}" if [[ -n "$override" ]]; then if [[ "$override" =~ ^[0-9]+$ && "$override" -ge 1 ]]; then export CARGO_BUILD_JOBS="$override" build_jobs_status="override:$override" return fi # Invalid override: warn and fall through to adaptive sizing. log "ignoring invalid job override '$override' (expected a positive integer); using adaptive sizing" unset CARGO_BUILD_JOBS fi # Adaptive sizing only on Linux where /proc/meminfo is available; elsewhere we # leave cargo to honor the .cargo/config.toml default. if [[ "$(uname -s)" != "Linux" || ! -r /proc/meminfo ]]; then build_jobs_status="cargo-default" return fi local cpus mem_available_kib mib_per_job_default mib_per_job cpus=$(cpu_count) mem_available_kib=$(meminfo_kib MemAvailable) [[ -n "$mem_available_kib" && "$mem_available_kib" =~ ^[0-9]+$ ]] || mem_available_kib=0 # Per-job memory budget (MiB). Sized with a cushion above the largest measured # rustc unit (jcode-base, ~1.6 GiB RSS sampled) so an idle machine uses nearly # every core while a memory-pressured one backs off before earlyoom kills a # build. Tunable per host via JCODE_BUILD_MIB_PER_JOB. mib_per_job_default=1792 mib_per_job="${JCODE_BUILD_MIB_PER_JOB:-$mib_per_job_default}" [[ "$mib_per_job" =~ ^[0-9]+$ && "$mib_per_job" -ge 256 ]] || mib_per_job="$mib_per_job_default" local mem_available_mib jobs_by_mem jobs mem_available_mib=$(( mem_available_kib / 1024 )) jobs_by_mem=$(( mem_available_mib / mib_per_job )) (( jobs_by_mem < 1 )) && jobs_by_mem=1 # Final job count is the smaller of "fits in memory" and core count. jobs="$jobs_by_mem" (( jobs > cpus )) && jobs="$cpus" (( jobs < 1 )) && jobs=1 export CARGO_BUILD_JOBS="$jobs" build_jobs_status="adaptive:${jobs} (cpus=${cpus}, mem_avail=${mem_available_mib}MiB, budget=${mib_per_job}MiB/job)" if (( jobs < cpus )); then log "limiting cargo to ${jobs} job(s) under memory pressure (${mem_available_mib}MiB available, ~${mib_per_job}MiB/job); override with JCODE_BUILD_JOBS" fi } # Keep `jcode-build-meta`'s embedded git metadata in lockstep with HEAD without # making it watch `.git` mtimes. # # `jcode-build-meta/build.rs` deliberately does NOT declare `.git/HEAD`/`.git/index` # as `rerun-if-changed` inputs, because their mtimes change on every `git add`, # `git status`, commit, and concurrent-agent git op -- which would force a # full-tree recompile (base -> app-core -> tui -> cli) on every incremental # build. The trade-off was that after a commit the binary kept embedding the # previous short hash, so the self-dev publish guard rejected it with # "binary was built from git hash X, but source state is Y" until someone # manually touched Cargo.toml. # # Instead, export the *value* of the current git hash/date. build.rs declares # `cargo:rerun-if-env-changed=JCODE_BUILD_GIT_HASH` (and _DATE), so cargo reruns # the build script ONLY when these values change -- i.e. exactly when HEAD moves # -- and never on a bare `git add`/`status` or repeated builds on the same # commit. This keeps the embedded hash correct after every commit while keeping # same-commit incremental builds fully incremental. We intentionally do NOT # export JCODE_BUILD_GIT_DIRTY here: the dirty flag flips on every edit and would # reintroduce per-build churn; the publish guard validates dirty builds via the # source fingerprint / mtime path instead of the embedded flag. export_git_build_metadata() { # Respect any value the caller already set (e.g. release/CI pipelines). if [[ -n "${JCODE_BUILD_GIT_HASH:-}" ]]; then git_meta_status="external:${JCODE_BUILD_GIT_HASH}" return fi if ! command -v git >/dev/null 2>&1; then git_meta_status="git-not-found" return fi local hash date hash="$(git -C "$repo_root" rev-parse --short HEAD 2>/dev/null || true)" if [[ -z "$hash" ]]; then git_meta_status="no-head" return fi export JCODE_BUILD_GIT_HASH="$hash" date="$(git -C "$repo_root" log -1 --format=%ci 2>/dev/null || true)" if [[ -n "$date" ]]; then export JCODE_BUILD_GIT_DATE="$date" fi git_meta_status="hash=${hash}" } maybe_configure_low_memory_selfdev() { if ! uses_selfdev_profile "$@"; then selfdev_low_memory_status="not-selfdev" return fi local mode="${JCODE_SELFDEV_LOW_MEMORY:-auto}" case "$mode" in 1|true|yes|on|force) ;; 0|false|no|off|never) selfdev_low_memory_status="disabled-by-env" return ;; auto|"") if ! selfdev_low_memory_default_needed; then selfdev_low_memory_status="auto-not-needed" return fi ;; *) printf 'error: unsupported JCODE_SELFDEV_LOW_MEMORY=%s (expected auto|on|off)\n' "$mode" >&2 exit 1 ;; esac export CARGO_INCREMENTAL="${CARGO_INCREMENTAL:-1}" export CARGO_PROFILE_SELFDEV_INCREMENTAL="${CARGO_PROFILE_SELFDEV_INCREMENTAL:-true}" export CARGO_PROFILE_SELFDEV_CODEGEN_UNITS="${CARGO_PROFILE_SELFDEV_CODEGEN_UNITS:-256}" # The low-memory profile deliberately keeps incremental compilation enabled # to avoid repeatedly rebuilding large root crates. sccache rejects Cargo # incremental builds, so default to direct rustc unless the caller opts out # of low-memory mode entirely. export SCCACHE_DISABLE="${SCCACHE_DISABLE:-1}" selfdev_low_memory_status="enabled:incremental=${CARGO_PROFILE_SELFDEV_INCREMENTAL},codegen-units=${CARGO_PROFILE_SELFDEV_CODEGEN_UNITS}" log "using low-memory selfdev overrides (${selfdev_low_memory_status#enabled:})" } # Enable rustc's parallel front-end (`-Zthreads`) for iterative dev/selfdev/test # builds. The jcode monoliths (jcode-base/app-core/tui) are ~80% single-threaded # front-end (type-check + borrow-check + monomorphization collection); at # opt-level 0 that front-end, not codegen, dominates wall time. The parallel # front-end is a nightly-only `-Z` flag, so this is gated on a nightly toolchain # being available and only applies to the unoptimized iteration profiles. # # Measured on this repo (Intel Ultra 7, 8 logical cores, selfdev profile): # jcode-base clean recompile 25.3s -> 12.7s (-Zthreads=4) # base-edit full-chain rebuild ~16s -> ~10s # Cranelift was tried too and was *slower* here (16.5s) because the bottleneck is # the front-end, not codegen, so we deliberately do not enable it. # # Controls: # JCODE_PARALLEL_FRONTEND=auto|0|1 (default auto) # JCODE_FRONTEND_THREADS= (default 4; diminishing returns past 4) # JCODE_DEV_TOOLCHAIN= (default: nightly when present) parallel_frontend_status="disabled" parallel_frontend_toolchain="" dev_nightly_toolchain() { # Prefer an explicit override, else a `+toolchain` already on the argv, else # the first installed nightly toolchain. if [[ -n "${JCODE_DEV_TOOLCHAIN:-}" ]]; then printf '%s\n' "$JCODE_DEV_TOOLCHAIN" return 0 fi local tc tc=$(rustup toolchain list 2>/dev/null | awk '/^nightly/ {print $1; exit}') tc=${tc%% *} [[ -n "$tc" ]] && printf '%s\n' "$tc" return 0 } configure_parallel_frontend() { local requested="${JCODE_PARALLEL_FRONTEND:-auto}" local forced="false" case "$requested" in 0|false|no|off) parallel_frontend_status="disabled-by-env" return 0 ;; 1|true|yes|on|force) forced="true" ;; auto) ;; *) parallel_frontend_status="disabled-bad-env:${requested}" return 0 ;; esac # Only worth it for the unoptimized iteration profiles where the front-end is # the bottleneck; release/release-lto keep their own (codegen-bound) path. # # By default (`auto`) we restrict to the `selfdev` profile: it builds into the # isolated `target/selfdev` dir that only this script + `selfdev build` use, so # adding `-Zthreads` to RUSTFLAGS (which changes cargo's unit fingerprint) # cannot thrash rust-analyzer's `target/debug` cache. Forcing the flag on # (`JCODE_PARALLEL_FRONTEND=1`) opts dev/test in too, accepting that potential # cache contention. local profile profile=$(selected_profile "$@") case "$profile" in selfdev) ;; dev|test) if [[ "$forced" != "true" ]]; then parallel_frontend_status="skipped-profile-shared-target:${profile}" return 0 fi ;; *) parallel_frontend_status="skipped-profile:${profile}" return 0 ;; esac # If the caller already pinned a toolchain via `cargo +foo`, don't override it. for arg in "$@"; do case "$arg" in +*) parallel_frontend_status="skipped-explicit-toolchain:${arg}" return 0 ;; esac done command -v rustup >/dev/null 2>&1 || { parallel_frontend_status="skipped-no-rustup" return 0 } local tc tc=$(dev_nightly_toolchain) if [[ -z "$tc" ]]; then parallel_frontend_status="skipped-no-nightly" return 0 fi # Confirm the toolchain actually resolves (installed, not just configured). if ! rustup run "$tc" rustc --version >/dev/null 2>&1; then parallel_frontend_status="skipped-nightly-unavailable:${tc}" return 0 fi local threads="${JCODE_FRONTEND_THREADS:-4}" [[ "$threads" =~ ^[0-9]+$ && "$threads" -ge 1 ]] || threads=4 parallel_frontend_toolchain="$tc" export RUSTUP_TOOLCHAIN="$tc" append_rustflags "-Zthreads=${threads}" parallel_frontend_status="enabled:${tc}:threads=${threads}" log "using parallel rustc front-end (${tc}, -Zthreads=${threads})" } configure_linux_linker() { local requested_mode="${JCODE_FAST_LINKER:-auto}" local mode="$requested_mode" case "$mode" in auto) # Prefer mold over lld: on this repo's large statically-linked binary # (~300 MB .text), mold links the jcode bin in ~2.0s vs lld's ~2.9s # (measured, warm, selfdev profile). The bin relinks on every build, so # that ~0.8s is a per-build win. Both need clang as the linker driver. if command -v mold >/dev/null 2>&1 && command -v clang >/dev/null 2>&1; then mode="mold" elif command -v ld.lld >/dev/null 2>&1 && command -v clang >/dev/null 2>&1; then mode="lld" else mode="system" fi ;; lld|mold|system) ;; *) printf 'error: unsupported JCODE_FAST_LINKER=%s (expected auto|lld|mold|system)\n' "$mode" >&2 exit 1 ;; esac selected_linker_mode="$mode" export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER="${CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER:-clang}" case "$mode" in lld) append_rustflags "-C link-arg=-fuse-ld=lld" selected_linker_desc="clang + lld" log "using clang + lld" ;; mold) append_rustflags "-C link-arg=-fuse-ld=mold" selected_linker_desc="clang + mold" log "using clang + mold" ;; system) selected_linker_desc="system linker settings" if [[ "$requested_mode" == "auto" ]]; then log "no supported fast linker detected; using system linker settings" else log "using system linker settings" fi ;; esac } print_setup() { if [[ -n "${JCODE_DEV_FEATURE_PROFILE:-}" && "${JCODE_DEV_FEATURE_PROFILE}" != "default" ]]; then feature_profile_status="${JCODE_DEV_FEATURE_PROFILE}" fi cat <} feature_profile_status=$feature_profile_status git_meta_status=$git_meta_status build_git_hash=${JCODE_BUILD_GIT_HASH:-} rustc_wrapper=${RUSTC_WRAPPER:-} linker_mode=$selected_linker_mode linker_desc=${selected_linker_desc:-} linker=${CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER:-} rustflags=${CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS:-} EOF } remote_connect_timeout() { local value="${JCODE_REMOTE_CONNECT_TIMEOUT:-5}" if [[ ! "$value" =~ ^[0-9]+$ || "$value" -lt 1 ]]; then value=5 fi printf '%s\n' "$value" } remote_tcp_timeout() { # Bounded probe used the first time we contact a host (or after the recovery # window expires) so an unreachable host fails fast instead of waiting for # the full SSH ConnectTimeout. Accepts fractional seconds (GNU timeout). local value="${JCODE_REMOTE_TCP_TIMEOUT:-1}" if [[ ! "$value" =~ ^[0-9]+([.][0-9]+)?$ ]]; then value=1 fi printf '%s\n' "$value" } remote_recovery_tcp_timeout() { # Shorter probe used while a host was recently seen down. An up host always # answers in a few ms, so this still detects recovery on the next build while # keeping per-build cost low during an outage. local value="${JCODE_REMOTE_RECOVERY_TCP_TIMEOUT:-0.3}" if [[ ! "$value" =~ ^[0-9]+([.][0-9]+)?$ ]]; then value=0.3 fi printf '%s\n' "$value" } remote_down_cache_ttl() { # How long (seconds) a recent failure keeps using the shorter recovery probe # timeout. Recovery is still detected on the very next build; this only # controls how long downtime builds stay cheap before reverting to the full # probe timeout. Set to 0 to always use the full timeout. local value="${JCODE_REMOTE_DOWN_TTL:-300}" if [[ ! "$value" =~ ^[0-9]+$ ]]; then value=300 fi printf '%s\n' "$value" } remote_down_cache_path() { local remote="${JCODE_REMOTE_HOST:-}" local key if command -v cksum >/dev/null 2>&1; then key="$(printf '%s' "$remote" | cksum | awk '{print $1}')" else key="${remote//[^A-Za-z0-9_-]/_}" fi printf '%s/jcode-remote-down-%s\n' "${TMPDIR:-/tmp}" "$key" } remote_down_cache_fresh() { local ttl ttl="$(remote_down_cache_ttl)" [[ "$ttl" -gt 0 ]] || return 1 local cache cache="$(remote_down_cache_path)" [[ -f "$cache" ]] || return 1 local recorded now age recorded="$(cat "$cache" 2>/dev/null || printf '0')" [[ "$recorded" =~ ^[0-9]+$ ]] || return 1 now="$(date +%s)" age=$((now - recorded)) (( age >= 0 && age < ttl )) } record_remote_down() { local ttl ttl="$(remote_down_cache_ttl)" [[ "$ttl" -gt 0 ]] || return 0 local cache cache="$(remote_down_cache_path)" date +%s >"$cache" 2>/dev/null || true } clear_remote_down() { local cache cache="$(remote_down_cache_path)" rm -f "$cache" 2>/dev/null || true } # Resolve the effective hostname/port for the remote alias and report whether # the connection goes through a ProxyJump/ProxyCommand (where a direct TCP # probe to the final hostname would be misleading). remote_resolve_endpoint() { local ssh_bin="$1" remote="$2" local hostname="$remote" port=22 uses_proxy="false" local config line key value if config="$("$ssh_bin" -G "$remote" 2>/dev/null)"; then while IFS=' ' read -r key value; do case "$key" in hostname) [[ -n "$value" ]] && hostname="$value" ;; port) [[ "$value" =~ ^[0-9]+$ ]] && port="$value" ;; proxyjump|proxycommand) [[ -n "$value" && "$value" != "none" ]] && uses_proxy="true" ;; esac done <<<"$config" fi printf '%s\t%s\t%s\n' "$hostname" "$port" "$uses_proxy" } # Fast TCP reachability check using bash /dev/tcp with a hard timeout. remote_tcp_reachable() { local hostname="$1" port="$2" local tcp_timeout="${3:-}" if [[ -z "$tcp_timeout" ]]; then tcp_timeout="$(remote_tcp_timeout)" fi timeout "$tcp_timeout" bash -c "exec 3<>/dev/tcp/$hostname/$port" 2>/dev/null } remote_cargo_preflight() { local remote="${JCODE_REMOTE_HOST:-}" if [[ -z "$remote" ]]; then log "remote cargo requested but JCODE_REMOTE_HOST is not configured" return 1 fi local ssh_bin="${JCODE_REMOTE_SSH_BIN:-ssh}" if ! command -v "$ssh_bin" >/dev/null 2>&1; then log "remote cargo requested but ssh binary is unavailable: $ssh_bin" return 1 fi # Choose probe timeout: while a host was recently seen down, use a short # recovery timeout so downtime builds stay cheap. An up host answers in a few # ms regardless, so recovery is still detected on the very next build. local tcp_timeout if remote_down_cache_fresh; then tcp_timeout="$(remote_recovery_tcp_timeout)" else tcp_timeout="$(remote_tcp_timeout)" fi # Fast TCP pre-probe to fail fast when the host is offline, unless the # connection is proxied (where a direct probe would be wrong) or explicitly # disabled via JCODE_REMOTE_TCP_PROBE=0. local tcp_probe="${JCODE_REMOTE_TCP_PROBE:-1}" case "$tcp_probe" in 0|false|no|off) tcp_probe="0" ;; *) tcp_probe="1" ;; esac if [[ "$tcp_probe" == "1" ]]; then local endpoint hostname port uses_proxy endpoint="$(remote_resolve_endpoint "$ssh_bin" "$remote")" IFS=$'\t' read -r hostname port uses_proxy <<<"$endpoint" if [[ "$uses_proxy" != "true" ]]; then if ! remote_tcp_reachable "$hostname" "$port" "$tcp_timeout"; then log "remote host $remote ($hostname:$port) unreachable within ${tcp_timeout}s TCP probe; using local cargo" record_remote_down return 1 fi fi fi local connect_timeout connect_timeout="$(remote_connect_timeout)" local server_alive_interval="${JCODE_REMOTE_SERVER_ALIVE_INTERVAL:-10}" local server_alive_count="${JCODE_REMOTE_SERVER_ALIVE_COUNT_MAX:-1}" local output if ! output=$("$ssh_bin" \ -o BatchMode=yes \ -o ConnectTimeout="$connect_timeout" \ -o ServerAliveInterval="$server_alive_interval" \ -o ServerAliveCountMax="$server_alive_count" \ "$remote" "printf 'jcode-remote-ok\\n'" 2>&1); then log "remote cargo preflight failed for $remote after ~${connect_timeout}s: $output" record_remote_down return 1 fi clear_remote_down return 0 } remote_cargo_fallback_mode() { local mode="${JCODE_REMOTE_CARGO_FALLBACK:-local}" case "$mode" in local|1|true|yes|on) printf 'local\n' ;; error|fail|0|false|no|off|never) printf 'error\n' ;; *) printf 'error: unsupported JCODE_REMOTE_CARGO_FALLBACK=%s (expected local|error)\n' "$mode" >&2 exit 2 ;; esac } cargo_test_has_explicit_filter() { [[ "${1:-}" == "test" ]] || return 1 local expect_value="" shift for arg in "$@"; do if [[ -n "$expect_value" ]]; then expect_value="" continue fi case "$arg" in --) return 1 ;; --bench|--bin|--example|--features|--manifest-path|--message-format|--package|-p|--profile|--target|--target-dir) expect_value="$arg" ;; --bench=*|--bin=*|--example=*|--features=*|--manifest-path=*|--message-format=*|--package=*|-p=*|--profile=*|--target=*|--target-dir=*) ;; --*) ;; -*) ;; *) return 0 ;; esac done return 1 } run_local_cargo() { if cargo_test_has_explicit_filter "${cargo_argv[@]}" && [[ "${JCODE_DEV_CARGO_ALLOW_ZERO_TESTS:-0}" != "1" ]]; then local output_file output_file=$(mktemp "${TMPDIR:-/tmp}/jcode-dev-cargo.XXXXXX") local status=0 cargo "${cargo_argv[@]}" 2>&1 | tee "$output_file" || status=${PIPESTATUS[0]} if [[ "$status" -eq 0 ]] \ && grep -qE '^running 0 tests$' "$output_file" \ && ! grep -qE '^running [1-9][0-9]* tests$' "$output_file"; then printf 'dev_cargo: explicit cargo test filter matched zero tests; check the test path/name or set JCODE_DEV_CARGO_ALLOW_ZERO_TESTS=1 to allow this intentionally\n' >&2 rm -f "$output_file" return 97 fi rm -f "$output_file" return "$status" fi exec cargo "${cargo_argv[@]}" } validate_feature_profile export_git_build_metadata maybe_configure_low_memory_selfdev "$@" maybe_enable_sccache "$@" configure_parallel_frontend "$@" select_build_jobs if [[ "$(uname -s)" == "Linux" ]] && [[ "$(uname -m)" == "x86_64" ]]; then configure_linux_linker fi if [[ "${1:-}" == "--print-setup" ]]; then print_setup exit 0 fi cargo_argv=() while IFS= read -r -d '' arg; do cargo_argv+=("$arg") done < <(build_cargo_argv "$@") if [[ "${JCODE_REMOTE_CARGO:-0}" == "1" ]]; then if remote_cargo_preflight; then log "using remote cargo via scripts/remote_build.sh" exec "$repo_root/scripts/remote_build.sh" "${cargo_argv[@]}" fi if [[ "$(remote_cargo_fallback_mode)" == "local" ]]; then log "remote cargo unavailable; falling back to local cargo (set JCODE_REMOTE_CARGO_FALLBACK=error to fail instead)" else log "remote cargo unavailable and fallback disabled" exit 75 fi fi run_local_cargo