Initial modern minimal jsh build
ober
95d5a47af2020ba2dbd68b4921377deb8469b713
new file mode 100644 --- /dev/null +++ b/.build.yml @@ -0,0 +1,29 @@ +image: debian/stable +packages: + - build-essential + - libncurses-dev + - uuid-dev + - libz-dev + - liblz4-dev + - libpcre2-dev + - libssl-dev + - libx11-dev + - pkg-config + - git + - curl + - unzip + - zip +sources: + - https://git.sr.ht/~lisp/jerboa + - https://git.sr.ht/~lisp/jerboa-shell +tasks: + - build-jerboa-tools: | + cd jerboa + # Build the multicall Jerboa tool artifact used by jerboa-shell. + make jerboa + - build-shell: | + cd jerboa-shell + JERBUILD=$HOME/jerboa/dist/jerbuild make jsh-compile + - test-shell: | + cd jerboa-shell + JERBUILD=$HOME/jerboa/dist/jerbuild make test new file mode 100644 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,40 @@ +# Build artifacts — rebuilt inside container +**/*.so +**/*.wpo +**/*.o +**/*.a +*.boot +jsh +jsh-musl +jsh-musl.sha256 +jsh-macos +jsh-macos.sha256 +jsh-linux-amd64 +jsh-linux-amd64.sha256 +jsh-freebsd +jsh-freebsd.sha256 +jsh-android +jsh-android.sha256 +jsh_program.h +jsh_petite_boot.h +jsh_scheme_boot.h +jsh_jsh_boot.h + +# Staging dirs — local build output, rebuilt fresh inside the container. +# _jerbuild-stage holds macOS ar-merge output incl. a mode-000 __.SYMDEF that +# breaks Podman's context tar if not excluded. +_jerbuild-stage/ +coreutils-stage/ +ssl-stage/ +aws-stage/ +android-stage/ + +# Vendored deps — cloned fresh inside container from GitHub +vendor/ + +# VCS +.git + +# Docker +Dockerfile +.dockerignore new file mode 100644 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.sha256 binary new file mode 100644 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,388 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + +permissions: + contents: write + +jobs: + test: + runs-on: ubuntu-latest + container: + image: jerboa21/jerboa:latest + steps: + - uses: actions/checkout@v4 + - name: Run unit tests + run: make test JERBOA=/build/mine/jerboa/lib SCHEME=scheme + + rust-musl: + runs-on: ubuntu-latest + container: + image: jerboa21/jerboa:latest + steps: + - uses: actions/checkout@v4 + - name: Cache Rust registry + uses: actions/cache@v4 + with: + path: ~/.cargo/registry + key: rust-musl-${{ hashFiles('patches/regex_native.rs', 'rust-coreutils/Cargo.lock') }} + restore-keys: rust-musl- + - name: Build Rust libs (x86_64-musl) + run: | + cd /build/mine/jerboa/jerboa-native-rs + cp $GITHUB_WORKSPACE/patches/regex_native.rs src/regex_native.rs + grep -q '#[cfg(feature = "duckdb")]' src/lib.rs || \ + sed -i 's/^mod duckdb_native;/#[cfg(feature = "duckdb")]\nmod duckdb_native;/' src/lib.rs + RUSTFLAGS="--remap-path-prefix $HOME/.cargo/registry/src=crate" \ + cargo build --release --target x86_64-unknown-linux-musl --no-default-features --features "tls crypto sqlite" + strip -S target/x86_64-unknown-linux-musl/release/libjerboa_native.a + cp target/x86_64-unknown-linux-musl/release/libjerboa_native.a $GITHUB_WORKSPACE/ + + cd $GITHUB_WORKSPACE/rust-coreutils + RUSTFLAGS="--remap-path-prefix $HOME/.cargo/registry/src=crate" \ + cargo build --release --target x86_64-unknown-linux-musl + strip -S target/x86_64-unknown-linux-musl/release/libjsh_coreutils.a + cp target/x86_64-unknown-linux-musl/release/libjsh_coreutils.a $GITHUB_WORKSPACE/ + - uses: actions/upload-artifact@v4 + with: + name: rust-libs-musl + path: | + libjerboa_native.a + libjsh_coreutils.a + retention-days: 7 + + rust-freebsd: + runs-on: ubuntu-latest + container: + image: jerboa21/jerboa:latest + steps: + - uses: actions/checkout@v4 + - name: Cache Rust registry + uses: actions/cache@v4 + with: + path: ~/.cargo/registry + key: rust-freebsd-${{ hashFiles('patches/regex_native.rs', 'rust-coreutils/Cargo.lock') }} + restore-keys: rust-freebsd- + - name: Install FreeBSD cross target + run: rustup target add x86_64-unknown-freebsd + - name: Cache FreeBSD sysroot + id: cache-freebsd-sysroot + uses: actions/cache@v4 + with: + path: /opt/freebsd-sysroot + key: freebsd-sysroot-14.4-v1 + - name: Install FreeBSD sysroot link libs + if: steps.cache-freebsd-sysroot.outputs.cache-hit != 'true' + run: | + # Some transitive deps (e.g. crc-fast via uucore) declare cdylib and + # therefore need the FreeBSD link libs (libexecinfo, libkvm, ...). + # Extract just the libs from the FreeBSD 14.4 base release tarball. + # 14.0 was removed from download.freebsd.org; 14.4 is the latest 14.x. + command -v curl >/dev/null || (apt-get update -qq && apt-get install -y -qq curl xz-utils) + mkdir -p /opt/freebsd-sysroot + curl -fsSL --retry 3 -o /tmp/freebsd-base.txz \ + https://download.freebsd.org/releases/amd64/14.4-RELEASE/base.txz + tar -xJf /tmp/freebsd-base.txz -C /opt/freebsd-sysroot ./lib ./usr/lib + rm -f /tmp/freebsd-base.txz + - name: Build Rust libs (x86_64-freebsd) + run: | + # libc.so on FreeBSD is a linker script that references /lib/libc.so.7 + # and /usr/lib/libc_nonshared.a as ABSOLUTE paths. Plain `-L` flags + # don't redirect absolute paths — only `--sysroot` does. Without it, + # ld looks at the host's /lib/libc.so.7 (which doesn't exist on the + # Linux runner) and the link fails. + SYSROOT_FLAGS="-C link-arg=--sysroot=/opt/freebsd-sysroot" + LIB_FLAGS="-L /opt/freebsd-sysroot/usr/lib -L /opt/freebsd-sysroot/lib" + + cd /build/mine/jerboa/jerboa-native-rs + cp $GITHUB_WORKSPACE/patches/regex_native.rs src/regex_native.rs + grep -q '#[cfg(feature = "duckdb")]' src/lib.rs || \ + sed -i 's/^mod duckdb_native;/#[cfg(feature = "duckdb")]\nmod duckdb_native;/' src/lib.rs + # Drop cdylib for jerboa-native: only the static .a is needed by jsh-freebsd. + sed -i 's/^crate-type = \["cdylib", "staticlib"\]/crate-type = ["staticlib"]/' Cargo.toml + RUSTFLAGS="--remap-path-prefix $HOME/.cargo/registry/src=crate $SYSROOT_FLAGS $LIB_FLAGS" \ + cargo build --release --target x86_64-unknown-freebsd --no-default-features + strip -S target/x86_64-unknown-freebsd/release/libjerboa_native.a + cp target/x86_64-unknown-freebsd/release/libjerboa_native.a $GITHUB_WORKSPACE/ + + cd $GITHUB_WORKSPACE/rust-coreutils + RUSTFLAGS="--remap-path-prefix $HOME/.cargo/registry/src=crate $SYSROOT_FLAGS $LIB_FLAGS" \ + cargo build --release --target x86_64-unknown-freebsd + strip -S target/x86_64-unknown-freebsd/release/libjsh_coreutils.a + cp target/x86_64-unknown-freebsd/release/libjsh_coreutils.a $GITHUB_WORKSPACE/ + - uses: actions/upload-artifact@v4 + with: + name: rust-libs-freebsd + path: | + libjerboa_native.a + libjsh_coreutils.a + retention-days: 7 + + rust-android: + runs-on: ubuntu-latest + container: + image: jerboa21/jerboa:latest + steps: + - uses: actions/checkout@v4 + - name: Cache Rust registry + uses: actions/cache@v4 + with: + path: ~/.cargo/registry + key: rust-android-${{ hashFiles('patches/regex_native.rs', 'rust-coreutils/Cargo.lock') }} + restore-keys: rust-android- + - name: Install Android cross target + run: rustup target add aarch64-unknown-linux-musl + - name: Cache zig + cargo-zigbuild + id: cache-zig + uses: actions/cache@v4 + with: + path: | + /opt/zig + /usr/local/bin/cargo-zigbuild + key: zig-0.13.0-zigbuild-0.22.3 + - name: Install zig + cargo-zigbuild + if: steps.cache-zig.outputs.cache-hit != 'true' + shell: bash + run: | + # Use zig as the cross-linker for aarch64-linux-musl. Zig bundles its + # own musl libc/headers and downloads from ziglang.org / GitHub release + # mirrors which are far more reliable than musl.cc (which routinely + # times out from GitHub runners). cargo-zigbuild wraps cargo to use + # zig as the linker, transparently handling musl cross-link. + command -v curl >/dev/null || (apt-get update -qq && apt-get install -y -qq curl xz-utils) + mkdir -p /opt/zig + ZIG_VERSION=0.13.0 + curl -fsSL --retry 5 --retry-delay 5 --max-time 300 \ + "https://ziglang.org/download/${ZIG_VERSION}/zig-linux-x86_64-${ZIG_VERSION}.tar.xz" \ + -o /tmp/zig.tar.xz + tar -xJf /tmp/zig.tar.xz -C /opt/zig --strip-components=1 + rm -f /tmp/zig.tar.xz + # Pre-built cargo-zigbuild binary (avoid 5-min cargo install build) + ZIGBUILD_VERSION=0.22.3 + curl -fsSL --retry 5 --retry-delay 5 --max-time 300 \ + "https://github.com/rust-cross/cargo-zigbuild/releases/download/v${ZIGBUILD_VERSION}/cargo-zigbuild-x86_64-unknown-linux-musl.tar.xz" \ + -o /tmp/cargo-zigbuild.tar.xz + tar -xJf /tmp/cargo-zigbuild.tar.xz -C /usr/local/bin/ + rm -f /tmp/cargo-zigbuild.tar.xz + - name: Build Rust libs (aarch64-musl) + run: | + export PATH=/opt/zig:/usr/local/bin:$PATH + cd /build/mine/jerboa/jerboa-native-rs + cp $GITHUB_WORKSPACE/patches/regex_native.rs src/regex_native.rs + grep -q '#[cfg(feature = "duckdb")]' src/lib.rs || \ + sed -i 's/^mod duckdb_native;/#[cfg(feature = "duckdb")]\nmod duckdb_native;/' src/lib.rs + sed -i '/SYS_LANDLOCK_RESTRICT_SELF.*446;$/a\\n#[cfg(target_arch = "aarch64")]\nconst SYS_LANDLOCK_CREATE_RULESET: libc::c_long = 444;\n#[cfg(target_arch = "aarch64")]\nconst SYS_LANDLOCK_ADD_RULE: libc::c_long = 445;\n#[cfg(target_arch = "aarch64")]\nconst SYS_LANDLOCK_RESTRICT_SELF: libc::c_long = 446;' src/landlock.rs + # Drop cdylib for cross-compile: only the static .a is needed by jsh-android. + sed -i 's/^crate-type = \["cdylib", "staticlib"\]/crate-type = ["staticlib"]/' Cargo.toml + RUSTFLAGS="--remap-path-prefix $HOME/.cargo/registry/src=crate" \ + cargo zigbuild --release --target aarch64-unknown-linux-musl --no-default-features + strip -S target/aarch64-unknown-linux-musl/release/libjerboa_native.a + cp target/aarch64-unknown-linux-musl/release/libjerboa_native.a $GITHUB_WORKSPACE/ + + cd $GITHUB_WORKSPACE/rust-coreutils + RUSTFLAGS="--remap-path-prefix $HOME/.cargo/registry/src=crate" \ + cargo zigbuild --release --target aarch64-unknown-linux-musl + strip -S target/aarch64-unknown-linux-musl/release/libjsh_coreutils.a + cp target/aarch64-unknown-linux-musl/release/libjsh_coreutils.a $GITHUB_WORKSPACE/ + - uses: actions/upload-artifact@v4 + with: + name: rust-libs-android + path: | + libjerboa_native.a + libjsh_coreutils.a + retention-days: 7 + + build: + needs: rust-musl + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + name: rust-libs-musl + path: prebuilt/ + - name: Install Podman + run: | + sudo apt-get update + sudo apt-get install -y podman + podman --version + - name: Build jsh-musl (Podman, pre-built Rust libs injected) + run: | + make podman \ + JSH_PREBUILT_NATIVE=prebuilt/libjerboa_native.a \ + JSH_PREBUILT_COREUTILS=prebuilt/libjsh_coreutils.a + - name: Verify binary + run: | + ls -lh jsh-musl + file jsh-musl + cat jsh-musl.sha256 + - uses: actions/upload-artifact@v4 + with: + name: jsh-musl + path: | + jsh-musl + jsh-musl.sha256 + if-no-files-found: error + retention-days: 30 + + build-freebsd: + needs: rust-freebsd + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + name: rust-libs-freebsd + path: prebuilt/ + - name: Install Podman + run: | + sudo apt-get update + sudo apt-get install -y podman + podman --version + - name: Build jsh-freebsd (Podman, pre-built Rust libs injected) + run: | + make freebsd-podman \ + JSH_PREBUILT_NATIVE=prebuilt/libjerboa_native.a \ + JSH_PREBUILT_COREUTILS=prebuilt/libjsh_coreutils.a + - name: Verify binary + run: | + ls -lh jsh-freebsd + file jsh-freebsd + cat jsh-freebsd.sha256 + - uses: actions/upload-artifact@v4 + with: + name: jsh-freebsd + path: | + jsh-freebsd + jsh-freebsd.sha256 + if-no-files-found: error + retention-days: 30 + + build-android: + needs: rust-android + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Podman and QEMU + run: | + sudo apt-get update + sudo apt-get install -y podman qemu-user-static binfmt-support + sudo update-binfmts --enable qemu-aarch64 || true + podman --version + - uses: actions/download-artifact@v4 + with: + name: rust-libs-android + path: prebuilt/ + - name: Build jsh-android (Podman, pre-built Rust libs injected) + run: | + make android-podman \ + JSH_PREBUILT_NATIVE=prebuilt/libjerboa_native.a \ + JSH_PREBUILT_COREUTILS=prebuilt/libjsh_coreutils.a + - name: Verify binary + run: | + ls -lh jsh-android + file jsh-android + cat jsh-android.sha256 + - uses: actions/upload-artifact@v4 + with: + name: jsh-android + path: | + jsh-android + jsh-android.sha256 + if-no-files-found: error + retention-days: 30 + + build-macos: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - name: Install dependencies + run: | + brew install openssl@3 lz4 + echo "PKG_CONFIG_PATH=$(brew --prefix openssl@3)/lib/pkgconfig" >> $GITHUB_ENV + - name: Resolve Chez Scheme HEAD SHA + id: chez-sha + run: | + SHA=$(git ls-remote https://github.com/ober/ChezScheme.git refs/heads/main | cut -f1) + echo "sha=$SHA" >> "$GITHUB_OUTPUT" + - name: Cache our Chez Scheme install + id: cache-chez + uses: actions/cache@v4 + with: + path: | + /opt/homebrew/lib/csv10.4.0-pre-release.4 + /opt/homebrew/bin/scheme + /opt/homebrew/bin/petite + /opt/homebrew/bin/scheme-script + key: chez-macos-arm64-${{ steps.chez-sha.outputs.sha }} + - name: Build our Chez Scheme fork + if: steps.cache-chez.outputs.cache-hit != 'true' + run: | + # jerboa-shell needs ober/ChezScheme (10.4.0-pre-release.4+) which adds + # base64-encode/decode, sha256-bytevector, etc. Stock Cisco Chez 10.3 + # and Homebrew chezscheme are too old. + git clone --depth 1 --branch main https://github.com/ober/ChezScheme.git /tmp/ChezScheme + cd /tmp/ChezScheme + ./configure --installprefix=/opt/homebrew + make -j$(sysctl -n hw.ncpu) + sudo make install + - name: Verify Chez Scheme + run: | + which scheme + scheme --version + echo '(display (top-level-bound? (quote base64-encode))) (newline)' | scheme --script /dev/stdin + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + - name: Cache Rust registry + uses: actions/cache@v4 + with: + path: ~/.cargo/registry + key: rust-macos-${{ hashFiles('patches/regex_native.rs', 'rust-coreutils/Cargo.lock') }} + restore-keys: rust-macos- + - name: Build jsh-macos + run: make jsh-macos + - name: Smoke-test binary + run: | + ls -lh jsh-macos + ./jsh-macos -c 'echo ok' + - uses: actions/upload-artifact@v4 + with: + name: jsh-macos + path: | + jsh-macos + jsh-macos.sha256 + if-no-files-found: error + retention-days: 30 + + release: + needs: [test, build, build-freebsd, build-android, build-macos] + if: github.ref == 'refs/heads/master' && github.event_name == 'push' + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + path: artifacts + merge-multiple: true + - name: Update rolling 'latest' release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + TAG=latest + gh release view "$TAG" --repo "${{ github.repository }}" >/dev/null 2>&1 || \ + gh release create "$TAG" \ + --repo "${{ github.repository }}" \ + --title "Latest static jsh binaries" \ + --notes "Auto-built by CI from latest master commit." \ + --target master + gh release upload "$TAG" \ + artifacts/jsh-musl artifacts/jsh-musl.sha256 \ + artifacts/jsh-freebsd artifacts/jsh-freebsd.sha256 \ + artifacts/jsh-android artifacts/jsh-android.sha256 \ + artifacts/jsh-macos artifacts/jsh-macos.sha256 \ + --repo "${{ github.repository }}" --clobber new file mode 100644 --- /dev/null +++ b/.gitignore @@ -0,0 +1,107 @@ +# Build artifacts +*.sls +*.so +*.wpo +*.o +*.boot +.jerbuild-hashes +*.tarm64osx +/jsh +/jsh-musl +/jsh-musl.sha256 +/jsh-android +/jsh-android.sha256 +/jsh-macos +/jsh-macos.sha256 +/jsh-macos-minimal +/jsh-macos-minimal.sha256 +/jsh-macos-full +/jsh-macos-full.sha256 +/jsh-freebsd +/jsh-freebsd.sha256 +/jsh-kernel +/jsh-linux-amd64 +/jsh-linux-amd64-main.c +/jsh-linux-arm64 +/jsh-linux-arm64-main.c +/jsh-freebsd-amd64 +/jsh-freebsd-amd64-main.c +/petite_boot.h +/scheme_boot.h +/jsh_libs_boot.h +/jsh_program.h +/qemu_*.core +gsh_program.h +jsh-all.so +libgsh-ffi.so + +# Auto-generated .sls files (built by build-jerboa.ss from .ss sources) +# Handwritten files are explicitly tracked via git add -f +src/jsh/arithmetic.sls +src/jsh/ast.sls +src/jsh/builtins.sls +src/jsh/completion.sls +src/jsh/control.sls +src/jsh/environment.sls +src/jsh/executor.sls +src/jsh/expander.sls +src/jsh/functions.sls +src/jsh/fuzzy.sls +src/jsh/fzf.sls +src/jsh/glob.sls +src/jsh/history.sls +src/jsh/jobs.sls +src/jsh/lexer.sls +src/jsh/lineedit.sls +src/jsh/macros.sls +src/jsh/main.sls +src/jsh/parser.sls +src/jsh/pipeline.sls +src/jsh/pregexp-compat.sls +src/jsh/prompt.sls +src/jsh/procwatch.sls +src/jsh/redirect.sls +src/jsh/registry.sls +src/jsh/script.sls +src/jsh/signals.sls +src/jsh/startup.sls +src/jsh/util.sls +src/jsh/player.sls +src/jsh/recorder.sls +src/jsh/embed-data.sls + +# Embedded files (may contain secrets) +embed/ + +# Vendor +_vendor/ +vendor/ +/jerboa-shell/ + +# Rust build artifacts +rust-coreutils/target/ +rust-coreutils/ripgrep-core/target/ + +# Android build staging +/android-stage/ +/aws-stage/ + +# macOS build vault staging (re-populated each build from upstream chez-fuse) +/vault-stage-macos/ + +# Legacy SSH build staging +/ssh-stage/ + +# Cache +.gerbil-lsp-cache/ +.jerboa/bin/ +jsh-freebsd +# jerbuild staging directory +/jsh-src/ +/_jerbuild-stage/ +# generated at build time from jsh-generate.ss +/jsh-generated.ss +# Claude Code session files (user-specific) +/.claude/ +*.dylib +.jcode/ new file mode 100644 --- /dev/null +++ b/.gitsafe.json @@ -0,0 +1,3 @@ +{ + "severity": "high" +} new file mode 100644 --- /dev/null +++ b/.jerboa/security.json @@ -0,0 +1,22 @@ +{ + "version": 1, + "repo": "jerboa-shell", + "extends": ["jerboa:cli", "jerboa:ffi", "jerboa:parser", "jerboa:generated-heavy"], + "paths": { + "production": ["*.ss", "*.sls", "lib/**/*.ss", "lib/**/*.sls", "src/**/*.{ss,sls,c,h,rs}", "support/**", "ffi-shim.c", "Makefile"], + "tests": ["test/**", "tests/**", "**/*-test.ss", "fixtures/**"], + "generated": ["build/**", "dist/**", "target/**", "_tmp/**", "jsh-generated.ss", "jsh-macos", "*.sha256", "*.so", "*.dylib", "*.wpo"], + "vendor": ["vendor/**", "third_party/**", "oils/**"], + "docs": ["README.md", "docs/**", "*.md", "AGENTS.md"] + }, + "policy": { + "failOn": ["critical", "high"], + "imports": { "directChezscheme": "allow-in-ffi-boundaries" }, + "ffi": { "allowed": true, "requireCloseOnExec": true, "requireDynamicWindCleanup": true }, + "process": { "shellInterpolation": "deny", "restrictedModeExpected": true }, + "network": { "requireTimeouts": true }, + "eval": { "stringEval": "deny", "bareRead": "deny", "allowReadEval": false }, + "shell": { "treatScriptsAsCodeExecution": true, "requireFdHygiene": true } + }, + "suppressions": [] +} new file mode 100644 --- /dev/null +++ b/.jerbuild @@ -0,0 +1,57 @@ +;; jerbuild build config for jerboa-shell (jsh) — macOS. +;; +;; All paths are relative to this file. The pre-build hook regenerates +;; jsh-generated.ss + stages stdlib overrides into _jerbuild-stage. +;; +;; Out: jsh-macos (matches what `make jsh-macos` produces). Run with +;; jerbuild build +;; from this directory. + +(entry "jsh-generated.ss") +(output "jsh-macos") +;; Requires: jerbuild, cargo/rustc, cc, and libssl/libcrypto. +;; The pre-build hook uses jerbuild's bundled Chez/std library, sparse-cloned +;; vendor/jerboa-native-rs, and cargo-built Rust archives. No Jerboa source +;; checkout is required. + +(libdirs "_jerbuild-stage" ; stdlib patches — must shadow bundle copies + "src" + "vendor/jerboa-coreutils/lib" + "vendor/jerboa-awk/lib" + "vendor/jerboa-sed/lib" + "vendor/jerboa-aws/lib" + "vendor/jerboa-wormhole" + "vendor/jerboa-yubikey/lib" + "vendor/jerboa-ssh/lib" + "vendor/jerboa-fuse/lib") + +;; FFI shim — implements all ffi_* foreign-procedure targets used by +;; (jsh ffi), (jsh limits), (std os landlock), etc. +(extra-sources "ffi-shim.c") + +;; Rust archive staged by support/stage-for-jerbuild.sh. It builds +;; libjerboa_native.a + libjsh_coreutils.a with Cargo, then merges them into +;; a single archive with duplicate Rust runtime symbols localized. +(extra-archives + "_jerbuild-stage/librust_combined.a" + ;; jerboa-ssh agent + transport C shims, staged by stage-for-jerbuild.sh. + "_jerbuild-stage/libjerboa_ssh.a" + ;; jerboa-fuse mount C shim (mount_helper.c), staged by stage-for-jerbuild.sh. + "_jerbuild-stage/libjerboa_fuse.a") + +;; jerboa-ssh + jerboa-crypto need libssl/libcrypto from Homebrew. libutil is +;; needed by openpty/forkpty used in (jsh pty). +(extra-ldflags "-L/opt/homebrew/lib" "-L/usr/local/lib" + "-lssl" "-lcrypto" "-lutil") + +;; Custom main.c — jsh bypasses Chez's arg parsing (JSH_ARG* env vars) +;; and uses Sscheme_script instead of Sscheme_program to avoid the +;; fork-thread / GC futex deadlock. +(main-c "support/jsh-jerbuild-main.c") + +;; FFI symbols registered via Sforeign_symbol after Sbuild_heap — static +;; macOS binaries can't resolve via dlsym(RTLD_DEFAULT). +(ffi-symbols "support/jsh-jerbuild-symbols.list") + +;; Pre-build: wipe stale .so/.wpo + patch stdlib + regen jsh-generated.ss. +(pre-build "support/stage-for-jerbuild.sh") new file mode 100644 --- /dev/null +++ b/.jerbuild.freebsd-amd64 @@ -0,0 +1,46 @@ +;; jerbuild build config for jerboa-shell (jsh) — native FreeBSD amd64. +;; +;; Build on a FreeBSD amd64 host with: +;; jerbuild build --config .jerbuild.freebsd-amd64 +;; +;; This uses only the installed jerbuild bundle, cc, and cargo. It deliberately +;; has no source-checkout, cross-compiler, or absolute runtime-path dependency. + +(entry "jsh-generated.ss") +(output "jsh-freebsd") + +(cc "cc") + +;; FreeBSD libc has dlopen, no separate -ldl. -lutil is for openpty. +;; Chez's expeditor needs termcap, and the packaged Chez kernel references +;; GNU libiconv from /usr/local/lib. +(os-libs "-lm -lpthread -lutil -ltermcap -L/usr/local/lib -liconv") + +(libdirs "_jerbuild-stage" ; stdlib patches — must shadow bundle copies + "src" + "vendor/jerboa-coreutils/lib" + "vendor/jerboa-awk/lib" + "vendor/jerboa-sed/lib" + "vendor/jerboa-aws/lib" + "../jerboa-wormhole" + "vendor/jerboa-yubikey/lib" + "vendor/jerboa-ssh/lib" + "vendor/jerboa-fuse/lib") + +;; FFI shim — same shim used by the macOS + Linux builds. Embed crypto symbols +;; come from the staged jerboa-native-rs archive. +(extra-sources "ffi-shim.c") + +;; Rust archive staged by support/stage-for-jerbuild-freebsd.sh. No coreutils +;; archive is linked on FreeBSD yet; freebsd-main.c provides weak stubs. +(extra-archives + "_jerbuild-stage/libjerboa_native.a" + "_jerbuild-stage/libjerboa_ssh.a" + "_jerbuild-stage/libjerboa_fuse.a") + +(extra-ldflags "-Wl,--export-dynamic") + +;; FreeBSD-specific main.c generated by the pre-build hook. +(main-c "_jerbuild-stage/freebsd-main.c") + +(pre-build "support/stage-for-jerbuild-freebsd.sh") new file mode 100644 --- /dev/null +++ b/.jerbuild.linux-amd64 @@ -0,0 +1,47 @@ +;; jerbuild build config for jerboa-shell (jsh) — native Linux amd64. +;; +;; Build on a Linux amd64 host with: +;; jerbuild build --config .jerbuild.linux-amd64 +;; +;; This intentionally uses only the installed jerbuild bundle, cc, and cargo. +;; There is no Jerboa source checkout, cross compiler, or absolute runtime path. + +(entry "jsh-generated.ss") +(output "jsh-linux-amd64") + +(cc "cc") + +(os-libs "-lm -ldl -lpthread -ltinfo") + +(libdirs "_jerbuild-stage" ; stdlib patches — must shadow bundle copies + "src" + "vendor/jerboa-coreutils/lib" + "vendor/jerboa-awk/lib" + "vendor/jerboa-sed/lib" + "vendor/jerboa-aws/lib" + "../jerboa-wormhole" + "vendor/jerboa-yubikey/lib" + "vendor/jerboa-ssh/lib" + "vendor/jerboa-fuse/lib") + +;; FFI shim — same shim used by the macOS build. Embed crypto symbols come +;; from the staged jerboa-native-rs archive. +(extra-sources "ffi-shim.c") + +;; Rust archives staged by support/stage-for-jerbuild-linux.sh. +(extra-archives + "_jerbuild-stage/libjerboa_native.a" + "_jerbuild-stage/libjsh_coreutils.a" + "_jerbuild-stage/libjerboa_ssh.a" + "_jerbuild-stage/libjerboa_fuse.a") + +;; --export-dynamic + Sforeign_symbol registrations in main.c are how Chez +;; foreign-procedure finds symbols. --allow-multiple-definition tolerates +;; duplicate Rust runtime symbols across the two static archives. +(extra-ldflags "-Wl,--export-dynamic" + "-Wl,--allow-multiple-definition") + +;; Linux-specific main.c is generated by the pre-build hook. +(main-c "_jerbuild-stage/linux-main.c") + +(pre-build "support/stage-for-jerbuild-linux.sh") new file mode 100644 --- /dev/null +++ b/.jerbuild.linux-amd64-native @@ -0,0 +1,35 @@ +;; Compatibility alias for older local commands. The canonical native Linux +;; config is .jerbuild.linux-amd64. + +(entry "jsh-generated.ss") +(output "jsh-linux-amd64") + +(cc "cc") + +(os-libs "-lm -ldl -lpthread -ltinfo") + +(libdirs "_jerbuild-stage" + "src" + "vendor/jerboa-coreutils/lib" + "vendor/jerboa-awk/lib" + "vendor/jerboa-sed/lib" + "vendor/jerboa-aws/lib" + "../jerboa-wormhole" + "vendor/jerboa-yubikey/lib" + "vendor/jerboa-ssh/lib" + "vendor/jerboa-fuse/lib") + +(extra-sources "ffi-shim.c") + +(extra-archives + "_jerbuild-stage/libjerboa_native.a" + "_jerbuild-stage/libjsh_coreutils.a" + "_jerbuild-stage/libjerboa_ssh.a" + "_jerbuild-stage/libjerboa_fuse.a") + +(extra-ldflags "-Wl,--export-dynamic" + "-Wl,--allow-multiple-definition") + +(main-c "_jerbuild-stage/linux-main.c") + +(pre-build "support/stage-for-jerbuild-linux.sh") new file mode 100644 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,466 @@ +## The Jerboa Language — Quick Reference + +Jerboa is a Scheme dialect built on Chez Scheme. It has legacy shell ancestry but is its own language. **All user-facing code is `.ss` files. Never write `.sls` files for the user** — those are internal implementation files. + +### File Structure + +Every Jerboa file looks like this: + +```scheme +(import (jerboa prelude)) ;; ONE import gives you the ENTIRE language +;; Optional extra imports for modules NOT in the prelude: +;; (import (std net request)) + +(def (my-function x y) + (+ x y)) + +(displayln (my-function 1 2)) +``` + +Run with: `scheme --libdirs lib --script file.ss` + +**NEVER** write `(library ...)` forms — that's `.sls` internal syntax. + +### Reader Syntax Extensions + +``` +[...] → plain parentheses — same as Chez Scheme +{method obj args} → (~ obj 'method args) — method dispatch +name: → keyword #:name +:std/sort → (std sort) — colon-style module path +#<<END ... END → heredoc string +``` + +Square brackets `[...]` are interchangeable with `(...)`, exactly like stock Chez Scheme. You can freely use them in bindings, match clauses, and anywhere you'd use parentheses: +```scheme +;; All of these are correct: +(let ([x 1] [y 2]) (+ x y)) +(for/collect ([x (in-range 5)]) (* x x)) +(match val ([list a b] (+ a b))) +(cond [(> x 0) "positive"] [else "non-positive"]) +``` + +### CRITICAL: Things That DO NOT EXIST in Jerboa/Chez + +Claude frequently hallucinates these from other Scheme implementations, Gambit, Racket, or R7RS training data. +**NONE of them are real in Jerboa/Chez. STOP and use the correct form.** + +#### AI Compatibility Aliases (these now work in the prelude) +The following names from other Scheme dialects are aliased in `(jerboa prelude)`: +- `hash-has-key?` → `hash-key?` (Racket) +- `hash-table-set!` → `hash-put!` (Racket) +- `directory-exists?` → `file-directory?` (Gambit) +- `eql?` → `eqv?` (Common Lisp) +- `random-integer` → `random` (Gambit) +- `read-line` → `get-line` wrapper (Gambit) — works with or without port arg +- `force-output` → `flush-output-port` wrapper (Gambit) — works with or without port arg +- `string-map` → char-level map (Racket/R7RS) — `(string-map f str)` + +#### Hallucinated Functions (still do NOT exist) +- `symbol<?` — use `(lambda (a b) (string<? (symbol->string a) (symbol->string b)))` +- `string-contains?` — use `(string-contains str sub)` (returns index or #f, NOT boolean) +- `define-struct` — use `(defstruct name (fields ...))` +- `raise` with a string — use `(error 'who "message" irritants ...)` +- `environment-bound?` — non-Jerboa. No direct Chez equivalent + +#### Non-Jerboa Scheme-isms (from training data — wrong in Jerboa) +- `time->seconds` — use `(time-second (current-time))` for epoch seconds +- `thread-sleep!` — Gambit. Use `(sleep (make-time 'time-duration 0 seconds))` +- `thread-yield` — no Chez equivalent. Use `(sleep (make-time 'time-duration 0 0))` as workaround +- `path-expand` with 2 args — Jerboa takes 1 arg. Use `(path-join base rel)` for 2-arg version +- `process-status` — non-Jerboa. Use `(std misc process)` API in Jerboa +- `user-info-home` — non-Jerboa. Use `(getenv "HOME")` +- `the-environment` — non-Jerboa. Use `(interaction-environment)` in Chez +- `condition/report-string` — non-Jerboa. Use `(with-output-to-string (lambda () (display-condition c)))` +- `make-class-type` — non-Jerboa. Use `(defstruct ...)` or `(defclass ...)` in Jerboa +- `string-subst` — non-Jerboa. Not in prelude. Use `(string-replace str old new)` or implement manually +- `open-fd-pair` — Gambit. Does not exist in Chez; requires different API + +#### R6RS/Racket-isms (wrong variant) +- `make-equal-hashtable` — R6RS. Use `(make-hash-table)` from Jerboa prelude +- `arithmetic-shift` — Racket. Use `(bitwise-arithmetic-shift n k)` or `(ash n k)` in Chez +- `pregexp-match` — Racket. Use `(std text regex)` or `(std pregexp)` API in Jerboa + +### CRITICAL: Common Arity Mistakes + +- `(list-of? pred)` → returns a PREDICATE. It takes 1 arg. Use: `((list-of? number?) lst)` +- `(maybe pred)` → returns a PREDICATE. It takes 1 arg. Use: `((maybe string?) val)` +- `(in-range end)` or `(in-range start end)` or `(in-range start end step)` — NOT `(in-range start step end)` +- `(hash-ref ht key)` or `(hash-ref ht key default)` — NOT `(hash-ref key ht)` +- `(string-split str delimiter)` where delimiter is a CHAR: `(string-split "a,b" #\,)` +- `(make-rwlock)` — takes **0 args**, NOT `(make-rwlock 'name)` +- `(path-expand path)` — takes **1 arg**, NOT `(path-expand rel base)`; use `path-join` for 2-arg +- `(sort predicate list)` — Chez arg order. NOT `(sort list predicate)` + +### Core Forms (all from `(import (jerboa prelude))`) + +#### Definitions +```scheme +(def x 42) ;; variable +(def (f x y) (+ x y)) ;; function +(def (f x (y 10)) body) ;; optional param with default +(def (f x . rest) body) ;; rest args +(def* f ((x) ...) ((x y) ...)) ;; multi-arity +(defrule (name pat) template) ;; macro +``` + +#### Data Structures +```scheme +(defstruct point (x y)) ;; → make-point, point?, point-x, point-y, point-x-set! +(defstruct (circle shape) (radius)) ;; inheritance (single only) +(defmethod (area (self circle)) body) ;; method on type +(~ obj 'method arg ...) ;; dispatch (or {method obj arg ...}) +(defrecord person (name age)) ;; struct + pretty-print + ->alist +(define-enum color (red green blue)) ;; → color-red, color?, color->name +``` + +#### Pattern Matching +```scheme +(match value + (42 "exact") ;; literal + ((list a b c) (+ a b c)) ;; list destructure + ((cons h t) h) ;; pair + ((? number?) "num") ;; predicate + ((? string? s) (string-upcase s)) ;; predicate + bind + ((and (? number?) (? positive?)) "positive number") + ((or "yes" "y") #t) + ((=> string->number n) n) ;; view pattern + (n (where (> n 0)) "positive") ;; guard + (_ "default")) ;; wildcard +``` + +#### Error Handling +```scheme +(try expr (catch (e) handler) (finally cleanup)) +(try expr (catch (error? e) handler)) +(unwind-protect body cleanup) +(with-resource (var init cleanup) body) +``` + +#### Result Type (Rust-inspired ok/err) +```scheme +(ok 42) (err "bad") (ok? r) (err? r) +(unwrap (ok 42)) ;; → 42 (raises on err) +(unwrap-or (err "x") 0) ;; → 0 +(map-ok f result) (map-err f result) +(and-then result f) ;; monadic bind +(try-result expr) ;; exceptions → (err condition) +(try-result* expr) ;; exceptions → (err "message string") +(sequence-results list-of-results) ;; → (ok list) or first (err) +(->? (ok 10) (+ 5) (* 2)) ;; → (ok 30), short-circuits on err +``` + +#### Iterators +```scheme +(for ((x (in-range 5))) (displayln x)) +(for/collect ((x (in-range 5))) (* x x)) ;; → (0 1 4 9 16) +(for/fold ((sum 0)) ((x (in-range 10))) (+ sum x)) ;; → 45 +(for/or ((x lst)) (and (pred? x) x)) ;; first truthy +(for/and ((x lst)) (pred? x)) ;; all truthy + +;; Iterators: in-list, in-vector, in-string, in-range, in-hash-keys, +;; in-hash-values, in-hash-pairs, in-naturals, in-indexed, +;; in-port, in-lines, in-chars, in-bytes, in-producer +``` + +#### Threading Macros +```scheme +(-> x (f a) (g b)) ;; thread first: (g (f x a) b) +(->> x (f a) (g b)) ;; thread last: (g b (f a x)) +(as-> x v (f v) (g v)) ;; named +(some-> x (f) (g)) ;; short-circuit on #f +(cond-> x test (f) t2 (g)) ;; conditional steps +(->? (ok x) (f) (g)) ;; result-aware thread first +``` + +#### Ergo Typing +```scheme +(: expr pred?) ;; checked cast +(using (p (make-point 1 2) : point?) + (+ p.x p.y)) ;; dot-access → (point-x p) etc. +((list-of? number?) '(1 2 3)) ;; predicate factory → #t +((maybe string?) #f) ;; accepts #f or string → #t +``` + +#### Hash Tables +```scheme +(def ht (make-hash-table)) +(hash-put! ht "key" "val") +(hash-ref ht "key") ;; error if missing +(hash-ref ht "key" "default") ;; with default +(hash-get ht "key") ;; → val or #f +(hash-key? ht "key") ;; → #t/#f +(hash-remove! ht "key") +(hash->list ht) (hash-keys ht) (hash-values ht) +(hash-for-each (lambda (k v) ...) ht) +(list->hash-table '(("a" . 1) ("b" . 2))) +``` + +#### Strings +```scheme