diff --git a/.github/actions/setup-llvm/action.yml b/.github/actions/setup-llvm/action.yml new file mode 100644 index 00000000..4f8caf40 --- /dev/null +++ b/.github/actions/setup-llvm/action.yml @@ -0,0 +1,189 @@ +# Shared toolchain setup for jobs that compile cx: installs LLVM 23 and the +# link-time dependencies, and exports LLVMDIR/EXTRA_CMAKE_* for the configure +# step. Used by both build.yml and release.yml so releases can never drift +# onto a different toolchain than CI tested. +name: Set up LLVM 23 and link dependencies +description: Install LLVM 23 plus zlib/zstd/DIA SDK and export CMake paths + +runs: + using: composite + steps: + # Cache downloaded .debs (LLVM, SDL build deps, glfw) in a + # user-owned dir; the default archive dir is root-owned and the + # cache action could not restore into it. Stale entries are safe: + # apt re-downloads anything not matching the package index. + # The lock file and partial/ dir stay excluded: apt recreates them + # as root, and tar cannot read them back as the runner user. + - name: Cache apt archives (Ubuntu) + if: runner.os == 'Linux' + uses: actions/cache@v5 + with: + path: | + /tmp/apt-archives + !/tmp/apt-archives/lock + !/tmp/apt-archives/partial + key: apt-archives-llvm-23-sdl-glfw + + - name: Redirect apt archives (Ubuntu) + if: runner.os == 'Linux' + shell: bash + run: | + mkdir -p /tmp/apt-archives/partial + echo 'Dir::Cache::Archives "/tmp/apt-archives";' | sudo tee /etc/apt/apt.conf.d/99-ci-cache + + - name: Download LLVM (Ubuntu) + if: runner.os == 'Linux' + shell: bash + run: | + wget https://apt.llvm.org/llvm.sh + chmod +x llvm.sh + sudo ./llvm.sh 23 all + echo "/usr/lib/llvm-23/bin" >> $GITHUB_PATH + + # Bottle downloads are content-addressed, so a static key is safe. + # Only the fetch is cached; install/link still runs (cheap, and the + # full cellar layout is too version-sensitive to restore blindly). + - name: Cache Homebrew downloads (macOS) + if: runner.os == 'macOS' + uses: actions/cache@v5 + with: + path: ~/Library/Caches/Homebrew + key: homebrew-downloads-llvm-23-sdl3-glfw + + - name: Download LLVM (macOS) + if: runner.os == 'macOS' + shell: bash + run: | + # Refresh Homebrew first: a stale snapshot installs an older LLVM + # bottle (e.g. 23.1.0, whose libclangDriver.a ships an LLVM-bitcode + # object that the system linker can't parse). + brew update + brew install llvm@23 + echo LLVMDIR=$(brew --prefix llvm@23) >> $GITHUB_ENV + + # Cache the extracted LLVM tree: re-downloading and + # double-extracting the tarball costs minutes on every Windows leg. + # Bump the key when the URL below changes. + - name: Cache LLVM (Windows) + if: runner.os == 'Windows' + uses: actions/cache@v5 + with: + path: clang+llvm-23.1.1-x86_64-pc-windows-msvc + key: llvm-windows-23.1.1 + + - name: Download LLVM (Windows) + if: runner.os == 'Windows' + shell: bash + run: | + # Reuse the cached tree only when it is complete; a cache entry + # saved from a failed extract must trigger a re-download, not a + # build against a corrupt tree. + if [ ! -f "clang+llvm-23.1.1-x86_64-pc-windows-msvc/bin/clang.exe" ]; then + rm -rf clang+llvm-23.1.1-x86_64-pc-windows-msvc + curl -L "https://github.com/llvm/llvm-project/releases/download/llvmorg-23.1.1/clang+llvm-23.1.1-x86_64-pc-windows-msvc.tar.xz" --output llvm.tar.xz && 7z x llvm.tar.xz && 7z x llvm.tar + fi + # The sed below converts the path to a Windows-style path + echo LLVMDIR="$(echo "$PWD/clang+llvm-23.1.1-x86_64-pc-windows-msvc" | sed -e 's/^\///' -e 's/\//\\/g' -e 's/^./\0:/')" >> $GITHUB_ENV + + # LLVM 22+ locates the DIA SDK via $VSINSTALLDIR (see llvm/cmake/modules/FindDIASDK.cmake), + # which is only set inside a Visual Studio developer environment. + # vs-shell is composite (no Node runtime); ilammy/msvc-dev-cmd is abandoned on node20. + - name: Set up MSVC environment (Windows) + if: runner.os == 'Windows' + uses: egor-tensin/vs-shell@v2 + + - name: Provide DIA SDK (Windows) + if: runner.os == 'Windows' + shell: bash + run: | + # LLVM's LLVMDebugInfoPDB target needs diaguids.lib. Its FindDIASDK + # module only looks at $VSINSTALLDIR, which may point at an install + # without it, so search wider and pass an explicit path if needed. + DIA_CMAKE_DIR="" + if [ -f "$VSINSTALLDIR/DIA SDK/lib/amd64/diaguids.lib" ]; then + echo "DIA SDK found via VSINSTALLDIR" + else + VSWHERE="C:/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe" + if [ -f "$VSWHERE" ]; then + mapfile -t VS_INSTALLS < <("$VSWHERE" -products '*' -property installationPath) + for INSTALL_PATH in "${VS_INSTALLS[@]}"; do + INSTALL_PATH_UNIX=$(echo "$INSTALL_PATH" | tr -d '\r' | sed -e 's/\\/\//g') + if [ -f "$INSTALL_PATH_UNIX/DIA SDK/lib/amd64/diaguids.lib" ]; then + DIA_CMAKE_DIR="$INSTALL_PATH_UNIX/DIA SDK" + break + fi + done + fi + if [ -z "$DIA_CMAKE_DIR" ]; then + # Fall back to the DIA files shipped in the Windows SDK, staged + # into the layout FindDIASDK expects. + DIA_LIB=$(ls -d "/c/Program Files (x86)/Windows Kits/10/Lib/"*"/um/x64/diaguids.lib" 2>/dev/null | sort -r | head -n 1 || true) + DIA_INC=$(echo "$DIA_LIB" | sed -e 's#/Lib/\(.*\)/um/x64/diaguids.lib#/Include/\1/um/dia2.h#') + if [ -n "$DIA_LIB" ] && [ -f "$DIA_INC" ]; then + DIA_STAGE="$RUNNER_TEMP/dia-sdk" + mkdir -p "$DIA_STAGE/include" "$DIA_STAGE/lib/amd64" + cp "$DIA_LIB" "$DIA_STAGE/lib/amd64/diaguids.lib" + cp "$DIA_INC" "$DIA_STAGE/include/dia2.h" + DIA_CMAKE_DIR=$(echo "$DIA_STAGE" | sed -e 's/\\/\//g') + fi + fi + if [ -z "$DIA_CMAKE_DIR" ]; then + echo "ERROR: DIA SDK with lib/amd64/diaguids.lib not found in VSINSTALLDIR, any VS instance, or the Windows Kits" + exit 1 + fi + echo "DIA SDK found at: $DIA_CMAKE_DIR" + echo "DIA_CMAKE_DIR=$DIA_CMAKE_DIR" >> $GITHUB_ENV + fi + + # LLVM 22+ links LLVMSupport against the ZLIB::ZLIB and zstd::libzstd_static + # imported targets, so findable ZLIB and zstd are required at configure + # time. Use the static triplets to match the /MT runtime used with the + # prebuilt LLVM. + # Note: vcpkg names the static libs zs/zsd, which FindZLIB only + # searches for with ZLIB_USE_STATIC_LIBS=ON. Similarly, LLVM's Findzstd + # module only defines zstd::libzstd_static for a library literally named + # zstd_static.lib, which vcpkg doesn't ship (only zstd.lib), so copy it. + # The vcpkg baseline moves with the runner image, so resolve it to + # a commit SHA for the cache key. A static key would freeze on the + # first binaries saved (actions/cache never re-saves on hit) and + # rebuild from source forever after any update. Falls back to a + # static key if the install is not a git checkout. + - name: Get vcpkg baseline (Windows) + if: runner.os == 'Windows' + id: vcpkg-baseline + shell: bash + run: echo "sha=$(git -C \"$VCPKG_INSTALLATION_ROOT\" rev-parse HEAD 2>/dev/null || echo unknown)" >> "$GITHUB_OUTPUT" + + # vcpkg restores the built ports from this binary cache instead of + # compiling them; each entry carries an ABI hash, so stale hits are + # impossible. + - name: Cache vcpkg binaries (Windows) + if: runner.os == 'Windows' + uses: actions/cache@v5 + with: + path: ${{ runner.temp }}/vcpkg-binary-cache + key: vcpkg-${{ steps.vcpkg-baseline.outputs.sha }}-zlib-zstd-static + + - name: Install zlib and zstd (Windows) + if: runner.os == 'Windows' + shell: bash + env: + VCPKG_BINARY_SOURCES: "clear;files,${{ runner.temp }}/vcpkg-binary-cache,readwrite" + run: | + vcpkg install zlib:x64-windows-static zstd:x64-windows-static + cp "$VCPKG_INSTALLATION_ROOT/installed/x64-windows-static/lib/zstd.lib" "$VCPKG_INSTALLATION_ROOT/installed/x64-windows-static/lib/zstd_static.lib" + ls "$VCPKG_INSTALLATION_ROOT/installed/x64-windows-static/lib" + echo "EXTRA_CMAKE_PREFIX=$VCPKG_INSTALLATION_ROOT/installed/x64-windows-static" >> $GITHUB_ENV + echo "EXTRA_CMAKE_ARGS=-DZLIB_USE_STATIC_LIBS=ON" >> $GITHUB_ENV + + # The lit/snippet/LSP suites spawn thousands of compiler and linker + # processes, each writing temporary objects and executables. Real-time + # Defender scanning of every one is a large Windows-only slowdown, so + # exclude the build tree, the repo, and the temp directories. The + # runner is ephemeral, so narrowing scan coverage here costs nothing. + - name: Exclude build and temp paths from Defender (Windows) + if: runner.os == 'Windows' + shell: powershell + continue-on-error: true + run: | + Add-MpPreference -ExclusionPath "${{ runner.workspace }}", "$env:GITHUB_WORKSPACE", "$env:RUNNER_TEMP", "$env:TEMP" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ae14b7dc..def4242d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,182 +20,8 @@ jobs: with: python-version: 3.12 - # Cache downloaded .debs (LLVM, SDL build deps, glfw) in a - # user-owned dir; the default archive dir is root-owned and the - # cache action could not restore into it. Stale entries are safe: - # apt re-downloads anything not matching the package index. - # The lock file and partial/ dir stay excluded: apt recreates them - # as root, and tar cannot read them back as the runner user. - - name: Cache apt archives (Ubuntu) - if: matrix.os == 'ubuntu-latest' - uses: actions/cache@v5 - with: - path: | - /tmp/apt-archives - !/tmp/apt-archives/lock - !/tmp/apt-archives/partial - key: apt-archives-llvm-23-sdl-glfw - - - name: Redirect apt archives (Ubuntu) - if: matrix.os == 'ubuntu-latest' - run: | - mkdir -p /tmp/apt-archives/partial - echo 'Dir::Cache::Archives "/tmp/apt-archives";' | sudo tee /etc/apt/apt.conf.d/99-ci-cache - - - name: Download LLVM (Ubuntu) - if: matrix.os == 'ubuntu-latest' - run: | - wget https://apt.llvm.org/llvm.sh - chmod +x llvm.sh - sudo ./llvm.sh 23 all - echo "/usr/lib/llvm-23/bin" >> $GITHUB_PATH - - # Bottle downloads are content-addressed, so a static key is safe. - # Only the fetch is cached; install/link still runs (cheap, and the - # full cellar layout is too version-sensitive to restore blindly). - - name: Cache Homebrew downloads (macOS) - if: matrix.os == 'macos-latest' - uses: actions/cache@v5 - with: - path: ~/Library/Caches/Homebrew - key: homebrew-downloads-llvm-23-sdl3-glfw - - - name: Download LLVM (macOS) - if: matrix.os == 'macos-latest' - run: | - # Refresh Homebrew first: a stale snapshot installs an older LLVM - # bottle (e.g. 23.1.0, whose libclangDriver.a ships an LLVM-bitcode - # object that the system linker can't parse). - brew update - brew install llvm@23 - echo LLVMDIR=$(brew --prefix llvm@23) >> $GITHUB_ENV - - # Cache the extracted LLVM tree: re-downloading and - # double-extracting the tarball costs minutes on every Windows leg. - # Bump the key when the URL below changes. - - name: Cache LLVM (Windows) - if: matrix.os == 'windows-latest' - uses: actions/cache@v5 - with: - path: clang+llvm-23.1.1-x86_64-pc-windows-msvc - key: llvm-windows-23.1.1 - - - name: Download LLVM (Windows) - if: matrix.os == 'windows-latest' - shell: bash - run: | - # Reuse the cached tree only when it is complete; a cache entry - # saved from a failed extract must trigger a re-download, not a - # build against a corrupt tree. - if [ ! -f "clang+llvm-23.1.1-x86_64-pc-windows-msvc/bin/clang.exe" ]; then - rm -rf clang+llvm-23.1.1-x86_64-pc-windows-msvc - curl -L "https://github.com/llvm/llvm-project/releases/download/llvmorg-23.1.1/clang+llvm-23.1.1-x86_64-pc-windows-msvc.tar.xz" --output llvm.tar.xz && 7z x llvm.tar.xz && 7z x llvm.tar - fi - # The sed below converts the path to a Windows-style path - echo LLVMDIR="$(echo "$PWD/clang+llvm-23.1.1-x86_64-pc-windows-msvc" | sed -e 's/^\///' -e 's/\//\\/g' -e 's/^./\0:/')" >> $GITHUB_ENV - - # LLVM 22+ locates the DIA SDK via $VSINSTALLDIR (see llvm/cmake/modules/FindDIASDK.cmake), - # which is only set inside a Visual Studio developer environment. - # vs-shell is composite (no Node runtime); ilammy/msvc-dev-cmd is abandoned on node20. - - name: Set up MSVC environment (Windows) - if: matrix.os == 'windows-latest' - uses: egor-tensin/vs-shell@v2 - - - name: Provide DIA SDK (Windows) - if: matrix.os == 'windows-latest' - shell: bash - run: | - # LLVM's LLVMDebugInfoPDB target needs diaguids.lib. Its FindDIASDK - # module only looks at $VSINSTALLDIR, which may point at an install - # without it, so search wider and pass an explicit path if needed. - DIA_CMAKE_DIR="" - if [ -f "$VSINSTALLDIR/DIA SDK/lib/amd64/diaguids.lib" ]; then - echo "DIA SDK found via VSINSTALLDIR" - else - VSWHERE="C:/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe" - if [ -f "$VSWHERE" ]; then - mapfile -t VS_INSTALLS < <("$VSWHERE" -products '*' -property installationPath) - for INSTALL_PATH in "${VS_INSTALLS[@]}"; do - INSTALL_PATH_UNIX=$(echo "$INSTALL_PATH" | tr -d '\r' | sed -e 's/\\/\//g') - if [ -f "$INSTALL_PATH_UNIX/DIA SDK/lib/amd64/diaguids.lib" ]; then - DIA_CMAKE_DIR="$INSTALL_PATH_UNIX/DIA SDK" - break - fi - done - fi - if [ -z "$DIA_CMAKE_DIR" ]; then - # Fall back to the DIA files shipped in the Windows SDK, staged - # into the layout FindDIASDK expects. - DIA_LIB=$(ls -d "/c/Program Files (x86)/Windows Kits/10/Lib/"*"/um/x64/diaguids.lib" 2>/dev/null | sort -r | head -n 1 || true) - DIA_INC=$(echo "$DIA_LIB" | sed -e 's#/Lib/\(.*\)/um/x64/diaguids.lib#/Include/\1/um/dia2.h#') - if [ -n "$DIA_LIB" ] && [ -f "$DIA_INC" ]; then - DIA_STAGE="$RUNNER_TEMP/dia-sdk" - mkdir -p "$DIA_STAGE/include" "$DIA_STAGE/lib/amd64" - cp "$DIA_LIB" "$DIA_STAGE/lib/amd64/diaguids.lib" - cp "$DIA_INC" "$DIA_STAGE/include/dia2.h" - DIA_CMAKE_DIR=$(echo "$DIA_STAGE" | sed -e 's/\\/\//g') - fi - fi - if [ -z "$DIA_CMAKE_DIR" ]; then - echo "ERROR: DIA SDK with lib/amd64/diaguids.lib not found in VSINSTALLDIR, any VS instance, or the Windows Kits" - exit 1 - fi - echo "DIA SDK found at: $DIA_CMAKE_DIR" - echo "DIA_CMAKE_DIR=$DIA_CMAKE_DIR" >> $GITHUB_ENV - fi - - # LLVM 22+ links LLVMSupport against the ZLIB::ZLIB and zstd::libzstd_static - # imported targets, so findable ZLIB and zstd are required at configure - # time. Use the static triplets to match the /MT runtime used with the - # prebuilt LLVM. - # Note: vcpkg names the static libs zs/zsd, which FindZLIB only - # searches for with ZLIB_USE_STATIC_LIBS=ON. Similarly, LLVM's Findzstd - # module only defines zstd::libzstd_static for a library literally named - # zstd_static.lib, which vcpkg doesn't ship (only zstd.lib), so copy it. - # The vcpkg baseline moves with the runner image, so resolve it to - # a commit SHA for the cache key. A static key would freeze on the - # first binaries saved (actions/cache never re-saves on hit) and - # rebuild from source forever after any update. Falls back to a - # static key if the install is not a git checkout. - - name: Get vcpkg baseline (Windows) - if: matrix.os == 'windows-latest' - id: vcpkg-baseline - shell: bash - run: echo "sha=$(git -C \"$VCPKG_INSTALLATION_ROOT\" rev-parse HEAD 2>/dev/null || echo unknown)" >> "$GITHUB_OUTPUT" - - # vcpkg restores the built ports from this binary cache instead of - # compiling them; each entry carries an ABI hash, so stale hits are - # impossible. - - name: Cache vcpkg binaries (Windows) - if: matrix.os == 'windows-latest' - uses: actions/cache@v5 - with: - path: ${{ runner.temp }}/vcpkg-binary-cache - key: vcpkg-${{ steps.vcpkg-baseline.outputs.sha }}-zlib-zstd-static - - - name: Install zlib and zstd (Windows) - if: matrix.os == 'windows-latest' - shell: bash - env: - VCPKG_BINARY_SOURCES: "clear;files,${{ runner.temp }}/vcpkg-binary-cache,readwrite" - run: | - vcpkg install zlib:x64-windows-static zstd:x64-windows-static - cp "$VCPKG_INSTALLATION_ROOT/installed/x64-windows-static/lib/zstd.lib" "$VCPKG_INSTALLATION_ROOT/installed/x64-windows-static/lib/zstd_static.lib" - ls "$VCPKG_INSTALLATION_ROOT/installed/x64-windows-static/lib" - echo "EXTRA_CMAKE_PREFIX=$VCPKG_INSTALLATION_ROOT/installed/x64-windows-static" >> $GITHUB_ENV - echo "EXTRA_CMAKE_ARGS=-DZLIB_USE_STATIC_LIBS=ON" >> $GITHUB_ENV - - # The lit/snippet/LSP suites spawn thousands of compiler and linker - # processes, each writing temporary objects and executables. Real-time - # Defender scanning of every one is a large Windows-only slowdown, so - # exclude the build tree, the repo, and the temp directories. The - # runner is ephemeral, so narrowing scan coverage here costs nothing. - - name: Exclude build and temp paths from Defender (Windows) - if: matrix.os == 'windows-latest' - shell: powershell - continue-on-error: true - run: | - Add-MpPreference -ExclusionPath "${{ runner.workspace }}", "$env:GITHUB_WORKSPACE", "$env:RUNNER_TEMP", "$env:TEMP" + - name: Set up LLVM 23 and link dependencies + uses: ./.github/actions/setup-llvm - name: Create Build Environment run: cmake -E make_directory ${{runner.workspace}}/build @@ -654,6 +480,7 @@ jobs: run: | node website/test-bench.mjs node website/test-cx-mode.mjs + node website/test-install.mjs node wasm/test-editor.mjs node wasm/test-playground.mjs node wasm/test-playground-worker.mjs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..6f67ed6b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,235 @@ +name: Release + +# Prebuilt binaries behind the front-page download links. Pushing a v* tag +# publishes archives to GitHub Releases; pull requests touching this workflow +# or the shared LLVM setup still build, package, and smoke-test the archives +# (uploaded as artifacts) without publishing anything. +# +# Linux and Windows link LLVM statically, so their archives are standalone. +# macOS archives bundle the Homebrew LLVM dylibs next to the binaries +# (Homebrew ships shared-only libraries), with install names rewritten to +# @executable_path/@loader_path. The otool assert fails the release if any +# Homebrew reference survives. +on: + push: + tags: ["v[0-9]*"] + pull_request: + paths: + - .github/workflows/release.yml + - .github/actions/setup-llvm/** + workflow_dispatch: + +jobs: + package: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + # Older runner than CI on purpose: binaries must run on older + # distros than the build machine (glibc is forward-compatible only). + - os: ubuntu-22.04 + platform: linux-x64 + - os: macos-latest + platform: macos-arm64 + - os: windows-latest + platform: windows-x64 + env: + STAGE_DIR: stage/cx-${{ matrix.platform }} + steps: + - uses: actions/checkout@v5 + + - name: Set up LLVM 23 and link dependencies + uses: ./.github/actions/setup-llvm + + - name: Create Build Environment + run: cmake -E make_directory ${{runner.workspace}}/build + + - name: Configure CMake + shell: bash + working-directory: ${{runner.workspace}}/build + run: | + extra_args=() + if [ "$RUNNER_OS" = "Linux" ]; then + # Static libstdc++/libgcc: the archive must run on older distros + # than the build runner. Static LLVM/Clang: apt.llvm.org builds + # the LLVM dylib, which would otherwise link shared. + extra_args+=("-DCMAKE_EXE_LINKER_FLAGS=-static-libstdc++ -static-libgcc") + extra_args+=("-DCX_STATIC_LLVM=ON") + fi + if [ "$RUNNER_OS" = "Windows" ]; then + # Static CRT so users need no Visual C++ redistributable. The + # dumpbin assert below proves no CRT DLL dependency; if /MT ever + # stops applying, the release fails there instead of shipping a + # binary that refuses to start. + extra_args+=("-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded") + fi + cmake $GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH="$LLVMDIR${EXTRA_CMAKE_PREFIX:+;$EXTRA_CMAKE_PREFIX}" -DCMAKE_UNITY_BUILD=ON $EXTRA_CMAKE_ARGS ${DIA_CMAKE_DIR:+-DMSVC_DIA_SDK_DIR="$DIA_CMAKE_DIR"} "${extra_args[@]}" + + - name: Build cx and cx-lsp + working-directory: ${{runner.workspace}}/build + shell: bash + run: cmake --build . --config Release --target cx cx-lsp + + # The compiler finds its standard library next to the executable + # (see getCxRootDir), so the archive is just the two binaries plus + # a copy of std/. + - name: Stage archive contents + shell: bash + run: | + mkdir -p "$STAGE_DIR" + if [ "$RUNNER_OS" = "Windows" ]; then + cp "${{runner.workspace}}/build/Release/cx.exe" "${{runner.workspace}}/build/Release/cx-lsp.exe" "$STAGE_DIR/" + # lld-link powers native builds on machines with no C compiler on + # PATH (the driver prefers it next to cx.exe over the build-time + # LLVM tools dir, which doesn't exist on user machines). + cp "${LLVMDIR}\\bin\\lld-link.exe" "$STAGE_DIR/" + else + cp "${{runner.workspace}}/build/cx" "${{runner.workspace}}/build/cx-lsp" "$STAGE_DIR/" + strip "$STAGE_DIR/cx" "$STAGE_DIR/cx-lsp" + fi + cp -r "$GITHUB_WORKSPACE/std" "$STAGE_DIR/std" + cp "$GITHUB_WORKSPACE/LICENSE.txt" "$STAGE_DIR/" + + - name: Bundle macOS libraries + if: runner.os == 'macOS' + shell: bash + run: | + lib="$STAGE_DIR/lib" + mkdir -p "$lib" + # Copy every Homebrew library referenced by the binaries or by + # the bundled libraries themselves. Iterates to a fixpoint to + # catch transitive deps (z3, zstd, ...). + prev="unseeded" + while [ "$(ls "$lib" | tr '\n' ' ')" != "$prev" ]; do + prev="$(ls "$lib" | tr '\n' ' ')" + for f in "$STAGE_DIR"/cx "$STAGE_DIR"/cx-lsp "$lib"/*.dylib; do + [ -f "$f" ] || continue + otool -L "$f" | tail -n +2 | awk '{print $1}' | while read -r dep; do + case "$dep" in + # Copied once: cx and cx-lsp share dependencies, and -n + # would exit 1 on the second copy. A real cp failure must + # still fail the release, so guard with a test instead. + /opt/homebrew/* | /usr/local/*) + [ -e "$lib/${dep##*/}" ] || cp "$dep" "$lib/" + ;; + esac + done + done + done + rewire() { + otool -L "$1" | tail -n +2 | awk '{print $1}' | while read -r dep; do + case "$dep" in + /opt/homebrew/* | /usr/local/* | @rpath/*) + install_name_tool -change "$dep" "$2/$(basename "$dep")" "$1" + ;; + esac + done + } + for f in "$STAGE_DIR"/cx "$STAGE_DIR"/cx-lsp; do + rewire "$f" "@executable_path/lib" + # Drop Homebrew rpaths: an absolute fallback could mask a + # missed fixup, passing smoke on this machine only. + otool -l "$f" | awk '$1 == "path" {print $2}' | while read -r rpath; do + case "$rpath" in + /opt/homebrew/* | /usr/local/*) install_name_tool -delete_rpath "$rpath" "$f" || true ;; + esac + done + done + for d in "$lib"/*.dylib; do + rewire "$d" "@loader_path" + done + codesign --force -s - "$STAGE_DIR"/cx "$STAGE_DIR"/cx-lsp "$lib"/*.dylib + + # Hard fail before anything publishes: an archive that needs LLVM, + # libstdc++, Homebrew, or the MSVC redist installed is not a + # prebuilt binary. + - name: Assert standalone linking + shell: bash + run: | + if [ "$RUNNER_OS" = "Windows" ]; then + # Dash, not slash: git-bash would rewrite /dependents to a + # Windows path (C:\Program Files\Git\dependents). + deps=$(dumpbin -dependents "$STAGE_DIR/cx.exe" "$STAGE_DIR/cx-lsp.exe" "$STAGE_DIR/lld-link.exe") + echo "$deps" + if echo "$deps" | grep -Ei 'vcruntime|msvcp|msvcr'; then + echo "ERROR: binaries need the Visual C++ redistributable (/MT did not apply)" + exit 1 + fi + elif [ "$RUNNER_OS" = "macOS" ]; then + # The asset name promises arm64; fail loudly on runner drift. + [ "$(uname -m)" = "arm64" ] || { echo "ERROR: expected arm64 runner for macos-arm64 asset"; exit 1; } + otool -L "$STAGE_DIR"/cx "$STAGE_DIR"/cx-lsp "$STAGE_DIR"/lib/*.dylib + # Skip each file's own install-ID line (the line after each + # "file:" header): IDs are inert once nothing references them. + if otool -L "$STAGE_DIR"/cx "$STAGE_DIR"/cx-lsp "$STAGE_DIR"/lib/*.dylib | awk '/:$/{skip=1; next} skip==1{skip=0; next} {print}' | grep -E '/opt/homebrew|/usr/local|@rpath'; then + echo "ERROR: macOS binaries still reference Homebrew or @rpath" + exit 1 + fi + else + ldd "$STAGE_DIR/cx" "$STAGE_DIR/cx-lsp" + if ldd "$STAGE_DIR/cx" "$STAGE_DIR/cx-lsp" | grep -Ei 'llvm|clang|libstdc\+\+|libgcc'; then + echo "ERROR: binaries link LLVM, libstdc++, or libgcc dynamically" + exit 1 + fi + fi + + - name: Smoke-test the packaged binaries + shell: bash + run: | + # Run from a different directory than the binaries: this proves the + # packaged compiler finds its stdlib by executable path, not cwd. + printf 'void main() {\n println("smoke ok");\n}\n' > "$RUNNER_TEMP/hello.cx" + cd "$RUNNER_TEMP" + pkg="$GITHUB_WORKSPACE/$STAGE_DIR" + "$pkg/cx" run hello.cx + # Also exercise the native compile+link path (uses the system linker). + "$pkg/cx" hello.cx -o hello-native + ./hello-native + if [ "$RUNNER_OS" = "Windows" ]; then + # Bare-machine simulation: with no compiler on PATH, native builds + # must still link through the bundled lld-link. + PATH="/usr/bin:/bin" "$pkg/cx" hello.cx -o hello-embedded + ./hello-embedded + fi + "$pkg/cx-lsp" --help > /dev/null + + - name: Create archive + shell: bash + run: | + name="cx-${{ matrix.platform }}" + if [ "$RUNNER_OS" = "Windows" ]; then + (cd stage && 7z a "../$name.zip" "$name") + else + tar -czf "$name.tar.gz" -C stage "$name" + fi + + - name: Upload archive artifact + uses: actions/upload-artifact@v6 + with: + name: binaries-${{ matrix.platform }} + path: cx-${{ matrix.platform }}.* + + publish: + if: startsWith(github.ref, 'refs/tags/v') + needs: [package] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/download-artifact@v7 + with: + pattern: binaries-* + merge-multiple: true + path: dist + + - uses: softprops/action-gh-release@v2 + with: + files: dist/* + generate_release_notes: true + body: | + Prebuilt cx binaries for Linux (x64), macOS (Apple Silicon), and Windows (x64). Each archive holds `cx`, `cx-lsp`, and the standard library (Windows also bundles the lld linker). No installer, no dependencies: unpack and run. Native compilation needs a C toolchain: `cc` on PATH on Linux/macOS, Visual Studio Build Tools installed on Windows. + + Fastest install on Linux/macOS: `curl https://cx-language.github.io/cx/install.sh | bash` + + macOS notes: the archive bundles the LLVM libraries (~200MB unpacked), and the binaries are ad-hoc signed. The installer strips the download quarantine for you; if you unpack manually and macOS refuses to run them, right-click the binary and choose Open once. diff --git a/CMakeLists.txt b/CMakeLists.txt index bfdc0463..c8071df6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -73,6 +73,11 @@ if(CX_SANITIZE_LEAK) add_link_options(-fsanitize=leak) endif() +# Prebuilt binaries link LLVM and Clang statically so the archives run +# without an LLVM install. Without this, distributions that build the LLVM +# dylib (Debian/Ubuntu, Homebrew) link libLLVM/libclang-cpp shared. +option(CX_STATIC_LLVM "Link the LLVM/Clang static archives instead of the shared dylib" OFF) + # Instrumented build for the ASan gate (test/asan/check_asan.py), which runs # the compiler and repeated LSP analyses under AddressSanitizer. Needs a Clang # with -fsanitize=address (LLVM Clang or Apple Clang). @@ -131,7 +136,7 @@ endif() # --- zstd (real, required via LLVMSupport) --- find_package(zstd QUIET) -if(NOT TARGET zstd::libzstd_static) +if(NOT TARGET zstd::libzstd_static AND NOT TARGET zstd::libzstd_shared) message(STATUS "zstd not found, fetching zstd from source") FetchContent_Declare(zstd GIT_REPOSITORY https://github.com/facebook/zstd.git @@ -156,11 +161,24 @@ if(NOT TARGET zstd::libzstd_static) if(NOT TARGET zstd::libzstd_static) add_library(zstd::libzstd_static ALIAS libzstd_static) endif() + if(NOT TARGET zstd::libzstd_shared) + add_library(zstd::libzstd_shared ALIAS libzstd_static) + endif() # As above: keep our unity-build default out of the zstd subbuild. set_target_properties(libzstd_static PROPERTIES UNITY_BUILD OFF) set(CMAKE_DISABLE_FIND_PACKAGE_zstd TRUE) endif() +# LLVM distributions disagree on the name: Debian/Ubuntu exports reference +# zstd::libzstd_shared, others zstd::libzstd_static. A system zstd that only +# provides one spelling aliases the other; both link the same library. +if(TARGET zstd::libzstd_static AND NOT TARGET zstd::libzstd_shared) + add_library(zstd::libzstd_shared ALIAS zstd::libzstd_static) +endif() +if(TARGET zstd::libzstd_shared AND NOT TARGET zstd::libzstd_static) + add_library(zstd::libzstd_static ALIAS zstd::libzstd_shared) +endif() + # --- LibXml2 (dummy suffices; LLVMWindowsManifest is not linked) --- find_package(LibXml2 QUIET) if(NOT TARGET LibXml2::LibXml2) @@ -281,10 +299,15 @@ endif() add_executable(cx-lsp src/lsp/main.cpp) target_link_libraries(cx-lsp libcx) -if(LLVM_LINK_LLVM_DYLIB) +# plugins is only referenced by the Clang archives (PassPlugin::Load in +# clangFrontend): non-dylib builds pull it through the Clang link interfaces, +# so it must be named here for the dylib-edge rewrite below to reproduce that +# closure on dylib-building distributions. +llvm_map_components_to_libnames(CX_LLVM_COMPONENT_LIBS core native linker orcjit passes support plugins) +if(LLVM_LINK_LLVM_DYLIB AND NOT CX_STATIC_LLVM) set(LLVM_LIBS LLVM) else() - llvm_map_components_to_libnames(LLVM_LIBS core native linker orcjit passes support) + set(LLVM_LIBS ${CX_LLVM_COMPONENT_LIBS}) endif() # The LibXml2/DIASDK dummy targets above only satisfy LLVMExports.cmake; they # carry no library. Fail fast if a linked component ever needs the real one. @@ -295,7 +318,7 @@ if(CX_DIASDK_DUMMY AND "LLVMDebugInfoPDB" IN_LIST LLVM_LIBS) message(FATAL_ERROR "LLVMDebugInfoPDB requires the real DIA SDK, but the dummy target was used. Install the DIA SDK (Visual Studio) or point MSVC_DIA_SDK_DIR at it.") endif() -if(LLVM_LINK_LLVM_DYLIB AND TARGET clang-cpp) +if(LLVM_LINK_LLVM_DYLIB AND TARGET clang-cpp AND NOT CX_STATIC_LLVM) # Shared LLVM build: use only the shared libclang-cpp for the Clang C++ # API. It contains everything the static archives below have, so linking # both only slows the link and bloats the binary. (Homebrew's static @@ -305,6 +328,54 @@ if(LLVM_LINK_LLVM_DYLIB AND TARGET clang-cpp) else() list(APPEND LLVM_LIBS clangAST clangBasic clangFrontend clangLex clangParse clangSema clangDriver) endif() +# Dylib-building distributions (Debian/Ubuntu, Homebrew) export every Clang +# archive's link interface with a trailing `LLVM` edge onto the shared dylib, +# which would reintroduce the dylib even though LLVM_LIBS above names the +# static components. Static builds rewrite that edge to the components, +# keeping the other edges (transitive Clang archives) intact. The component +# set already covers every LLVM symbol the Clang archives need: non-dylib +# builds (Windows) link this exact combination. +if(CX_STATIC_LLVM AND LLVM_LINK_LLVM_DYLIB) + # Walk the Clang link closure so transitive Clang archives (which carry + # the same `LLVM` edge) are covered on any LLVM version. + set(CX_CLANG_LIBS clangAST clangBasic clangFrontend clangLex clangParse clangSema clangDriver) + set(CX_CLANG_SEEN "") + set(CX_CLANG_QUEUE ${CX_CLANG_LIBS}) + while(CX_CLANG_QUEUE) + list(POP_FRONT CX_CLANG_QUEUE CX_LIB) + if(CX_LIB IN_LIST CX_CLANG_SEEN OR NOT TARGET ${CX_LIB}) + continue() + endif() + list(APPEND CX_CLANG_SEEN ${CX_LIB}) + get_target_property(CX_IFACE ${CX_LIB} INTERFACE_LINK_LIBRARIES) + if(CX_IFACE) + foreach(dep IN LISTS CX_IFACE) + if(dep MATCHES "^clang") + list(APPEND CX_CLANG_QUEUE ${dep}) + elseif(dep MATCHES "^\\$$") + list(APPEND CX_CLANG_QUEUE ${CMAKE_MATCH_1}) + endif() + endforeach() + endif() + endwhile() + set(CX_REWRITTEN 0) + foreach(clang_lib IN LISTS CX_CLANG_SEEN) + get_target_property(clang_iface ${clang_lib} INTERFACE_LINK_LIBRARIES) + # The edge spells `LLVM` today; a LINK_ONLY spelling would silently + # reintroduce the dylib, so match that too. + if("LLVM" IN_LIST clang_iface OR "$" IN_LIST clang_iface) + list(REMOVE_ITEM clang_iface LLVM "$") + list(APPEND clang_iface ${CX_LLVM_COMPONENT_LIBS}) + list(REMOVE_DUPLICATES clang_iface) + set_target_properties(${clang_lib} PROPERTIES INTERFACE_LINK_LIBRARIES "${clang_iface}") + math(EXPR CX_REWRITTEN "${CX_REWRITTEN} + 1") + endif() + endforeach() + if(CX_REWRITTEN EQUAL 0) + message(FATAL_ERROR "CX_STATIC_LLVM found no LLVM dylib edge to rewrite; refusing to link shared silently") + endif() + message(STATUS "CX_STATIC_LLVM: rewrote LLVM dylib edge on ${CX_REWRITTEN} Clang targets") +endif() target_link_libraries(libcx ${LLVM_LIBS}) # Tests. Note: tests are run with CTest rather than custom build targets so diff --git a/README.md b/README.md index e3d2ee92..4ef3b3fc 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,9 @@ The [cx extension for VS Code](https://marketplace.visualstudio.com/items?itemNa ## Building from source -Compiling cx requires a C++20 compiler, [CMake](https://cmake.org) 3.16 or newer, +Prebuilt Linux, macOS, and Windows binaries are published on the [releases page](https://github.com/cx-language/cx/releases) +(the fastest install on Linux/macOS is `curl https://cx-language.github.io/cx/install.sh | bash`). +Otherwise, compiling cx requires a C++20 compiler, [CMake](https://cmake.org) 3.16 or newer, and [LLVM](https://llvm.org)/[Clang](https://clang.llvm.org) 23. ### Linux diff --git a/docs/050-hello-world.md b/docs/050-hello-world.md index 194e4bbd..fa04f8aa 100644 --- a/docs/050-hello-world.md +++ b/docs/050-hello-world.md @@ -2,8 +2,26 @@ ## Installing cx -Compiling cx requires a C++20 compiler, [CMake](https://cmake.org) 3.16 or newer, -and [LLVM](https://llvm.org)/[Clang](https://clang.llvm.org) 23. +The fastest install on Linux and macOS is the install script, which downloads +the prebuilt binary for your platform into `~/.cx` and smoke-tests it: + +```sh +curl https://cx-language.github.io/cx/install.sh | bash +``` + +It adds itself to your `PATH` (bash, zsh, and fish), so new shells pick it up +automatically; in the current shell, run the `export` it prints first. +Prebuilt binaries cover Linux (x64), macOS (Apple Silicon), and Windows (x64). +The Windows archive and manual downloads for all platforms are on the +[releases page](https://github.com/cx-language/cx/releases); each archive also +contains the [`cx-lsp` language server](./lsp) and the standard library. +Native compilation needs a C toolchain as well: `cc` on `PATH` on Linux/macOS, +or Visual Studio Build Tools installed on Windows (`cx run` JIT-compiles and +needs nothing). + +To build from source instead (required on Intel Macs and other platforms +without a prebuilt binary), you need a C++20 compiler, [CMake](https://cmake.org) +3.16 or newer, and [LLVM](https://llvm.org)/[Clang](https://clang.llvm.org) 23. On Ubuntu/Debian, install the prerequisites with: diff --git a/install.sh b/install.sh new file mode 100755 index 00000000..fb02bceb --- /dev/null +++ b/install.sh @@ -0,0 +1,104 @@ +#!/bin/sh +# Installs cx from GitHub Releases into ~/.cx (override with CX_HOME). +# Usage: curl https://cx-language.github.io/cx/install.sh | bash +# Pin a version with CX_VERSION=v0.1 (default: latest). +set -eu + +REPO="cx-language/cx" +VERSION="${CX_VERSION:-latest}" +DEST="${CX_HOME:-$HOME/.cx}" + +fail() { + echo "install.sh: $1" >&2 + exit 1 +} + +detect_platform() { + os=$(uname -s) + arch=$(uname -m) + case "$os" in + Linux) + case "$arch" in + x86_64) platform="linux-x64" ;; + *) fail "no prebuilt binary for Linux/$arch (only x64); build from source: https://cx-language.github.io/cx/hello-world" ;; + esac + ;; + Darwin) + case "$arch" in + arm64) platform="macos-arm64" ;; + *) fail "no prebuilt binary for Intel Macs; build from source: https://cx-language.github.io/cx/hello-world" ;; + esac + ;; + MINGW* | MSYS* | CYGWIN*) + fail "Windows: download cx-windows-x64.zip from https://github.com/$REPO/releases/latest and unpack it" + ;; + *) + fail "unsupported platform $os/$arch; build from source: https://cx-language.github.io/cx/hello-world" + ;; + esac +} + +release_url() { + if [ "$VERSION" = "latest" ]; then + echo "https://github.com/$REPO/releases/latest/download/cx-$platform.tar.gz" + else + echo "https://github.com/$REPO/releases/download/$VERSION/cx-$platform.tar.gz" + fi +} + +add_to_path() { + case ":$PATH:" in + *":$DEST:"*) echo "$DEST is already on your PATH."; return ;; + esac + line="export PATH=\"$DEST:\$PATH\"" + case "$(basename "${SHELL:-sh}")" in + bash) + if [ "$os" = "Darwin" ]; then rc="$HOME/.bash_profile"; else rc="$HOME/.bashrc"; fi + ;; + zsh) rc="$HOME/.zshrc" ;; + fish) + rc="$HOME/.config/fish/config.fish" + line="fish_add_path \"$DEST\"" + ;; + *) + echo "Put $DEST on your PATH, e.g.:" + echo " $line" + return + ;; + esac + mkdir -p "${rc%/*}" + if grep -qF "$DEST" "$rc" 2> /dev/null; then + echo "$DEST is already in $rc." + else + echo "$line" >> "$rc" + echo "Added $DEST to your PATH in $rc (takes effect in new shells)." + echo "For the current shell, run: $line" + fi +} + +command -v curl > /dev/null 2>&1 || fail "curl is required" +command -v tar > /dev/null 2>&1 || fail "tar is required" + +detect_platform +url=$(release_url) + +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT INT TERM + +echo "Downloading $url" +curl -fsSL -o "$tmp/cx.tar.gz" "$url" || fail "download failed: $url" + +mkdir -p "$DEST" +tar -xzf "$tmp/cx.tar.gz" -C "$DEST" --strip-components=1 + +if [ "$os" = "Darwin" ]; then + # Curl-downloaded files are quarantined; the ad-hoc signed binary + # would be blocked on first run without this. + xattr -dr com.apple.quarantine "$DEST" 2> /dev/null || true +fi + +printf 'void main() {\n println("install ok");\n}\n' > "$tmp/hello.cx" +"$DEST/cx" run "$tmp/hello.cx" > /dev/null || fail "smoke test failed" + +echo "Installed cx to $DEST." +add_to_path diff --git a/src/driver/driver.cpp b/src/driver/driver.cpp index 2f8c5e10..dc1364ee 100644 --- a/src/driver/driver.cpp +++ b/src/driver/driver.cpp @@ -852,6 +852,8 @@ int cx::buildModule(Module& mainModule, BuildParams buildParams) { // MSVC takes no -L/-l; its translated form is staged here (ccArgs borrows // from it, so it must outlive the link) and appended after -link below. std::vector msvcLinkStorage; + // Same for the bundled-tools -B flag below. + std::string bundledToolsFlag; if (isMSVC) { msvcLinkStorage = msvcLinkArgs(librarySearchPaths, libraries); } else { @@ -902,9 +904,16 @@ int cx::buildModule(Module& mainModule, BuildParams buildParams) { // Embedded Clang locates the MSVC and Windows SDK libraries itself; pass the CRT // libraries in -l form so they are found along its search paths, plus the same // 8MB stack reservation as the MSVC link above. + // Prebuilt installs bundle lld-link next to cx.exe: prefer it, since the + // build-time LLVM tools directory below doesn't exist on user machines. + std::string rootDir = getCxRootDir(); + if (!rootDir.empty() && llvm::sys::fs::exists(rootDir + "/lld-link.exe")) { + bundledToolsFlag = "-B" + rootDir; + ccArgs.push_back(bundledToolsFlag.c_str()); + } #ifdef CX_LLVM_TOOLS_DIR - // Point the driver at its own subprograms (lld-link): there are none next to - // cx.exe, and LLVM's bin directory is not on PATH on a bare machine. + // Point the driver at its own subprograms (lld-link): LLVM's bin + // directory is not on PATH on a bare machine. ccArgs.push_back("-B" CX_LLVM_TOOLS_DIR); #endif ccArgs.push_back("-l"); diff --git a/website/build-website.sh b/website/build-website.sh index 38e19cec..80b64467 100755 --- a/website/build-website.sh +++ b/website/build-website.sh @@ -272,6 +272,17 @@ if "##EXAMPLETABS##" in template: "##EXAMPLETABS##", '
' + buttons + more + "
" ) +# Button elements do not survive the pandoc round-trip (only their text +# does), so the install tabs are injected here, after rendering. +if "##OSTABS##" in template: + template = template.replace( + "##OSTABS##", + '
' + '' + '' + "
", + ) + # Inline markup inside code does not survive pandoc either, so grey the # shell prompts here. Only line-leading "$ "/"> " match, which cx code # never has (interpolation is always mid-line). @@ -325,6 +336,9 @@ python3 generate_search_index.py || exit cp -r *.css *.js lib build +# Installer served at /install.sh so the install command stays short. +cp ../install.sh build/ + # Gallery screenshots served from the site root. cp ../examples/fractal/screenshot.jpg build/fractal-screenshot.jpg cp ../examples/voxel-game/screenshot.jpg build/voxel-screenshot.jpg diff --git a/website/index.css b/website/index.css index d57730ff..f9e56eac 100644 --- a/website/index.css +++ b/website/index.css @@ -623,7 +623,8 @@ section { overflow-x: auto; } -.example-tabs button { +.example-tabs button, +.os-tabs button { font: inherit; font-size: 13px; background: transparent; @@ -636,7 +637,9 @@ section { } .example-tabs button[aria-pressed="true"], -.example-tabs button:hover { +.example-tabs button:hover, +.os-tabs button[aria-pressed="true"], +.os-tabs button:hover { background: var(--background-color-3); color: var(--text-color); } @@ -661,6 +664,12 @@ section { color: var(--text-color); } +.os-tabs { + display: flex; + gap: 4px; + margin-bottom: 20px; +} + /* The runnable editor keeps its docs-page look inside the window: flatten pandoc's wrapper so there is only one box. */ .window div.sourceCode { @@ -756,7 +765,8 @@ body.frontpage .part { scroll-margin-top: 84px; } -.cards { +.cards, +.steps { display: grid; grid-gap: 20px; } @@ -765,17 +775,24 @@ body.frontpage .part { grid-template-columns: repeat(2, 1fr); } +.steps { + grid-template-columns: repeat(3, 1fr); + counter-reset: step; +} + @media screen and (max-width: 800px) { - .cards { + .cards, + .steps { grid-template-columns: 1fr; } } -/* Cards with a leading heading come out of pandoc as sections +/* Cards with a leading heading and steps come out of pandoc as sections (it sectionizes classed heading-led divs; image-led gallery cards stay divs), so neutralize the base section margin. */ .card, -.cards > div { +.cards > div, +.step { min-width: 0; margin: 0; } @@ -817,12 +834,14 @@ body.frontpage .part { background-color: var(--background-color) !important; } -.cards h3 { +.cards h3, +.step h3 { font-size: 1rem; margin: 0 0 6px; } -.cards p { +.cards p, +.step p { font-size: 0.92rem; margin: 0 0 10px; } @@ -872,6 +891,23 @@ pre.snippet { margin: 0; } +.step { + padding-top: 12px; +} + +.step h3::before { + counter-increment: step; + content: counter(step) ". "; + color: var(--text-color-subtle); +} + +/* Long commands wrap instead of scrolling; copy-paste is unaffected. */ +.step pre.snippet { + white-space: pre-wrap; + overflow-wrap: anywhere; + margin-bottom: 10px; +} + .CodeMirror { font-family: inherit; background: transparent !important; diff --git a/website/index.html b/website/index.html index a446b219..e04baf4d 100644 --- a/website/index.html +++ b/website/index.html @@ -8,8 +8,9 @@

cx is a fast language that gets out of your way

simple C-like syntax, and type-safe constructs as efficient as hand-written C.

@@ -186,6 +187,56 @@

+
+

Installing

+

Prebuilt binaries for Linux (x64), macOS (Apple Silicon), and Windows (x64). To build from source, visit GitHub.

+ ##OSTABS## +
+
+
+

Install

+
$ curl https://cx-language.github.io/cx/install.sh | bash
+

Installs into ~/.cx, adds itself to your PATH, and smoke-tests the result.

+
+
+

Write hello.cx

+
void main() {
+    println("Hello world");
+}
+
+
+

Run it

+
$ export PATH="$HOME/.cx:$PATH"
+$ cx run hello.cx
+Hello world
+

New shells get the PATH automatically; this shell needs one export first. Then cx run JIT-compiles and executes in one step. Point your editor at the bundled cx-lsp - see Editor setup.

+
+
+
+ +
+
Language overview diff --git a/website/install.js b/website/install.js new file mode 100644 index 00000000..06743d2d --- /dev/null +++ b/website/install.js @@ -0,0 +1,24 @@ +// Front-page install tabs: show the instructions matching the visitor's OS. +// Scripts load synchronously before
parses, so wait for the DOM. +document.addEventListener("DOMContentLoaded", () => { + const tabs = document.querySelector(".os-tabs"); + if (!tabs) return; + const panes = document.querySelectorAll(".os-pane"); + + const show = (os) => { + tabs.querySelectorAll("button").forEach((button) => { + button.setAttribute("aria-pressed", button.getAttribute("data-os") === os ? "true" : "false"); + }); + panes.forEach((pane) => { + pane.hidden = pane.getAttribute("data-os") !== os; + }); + }; + + tabs.addEventListener("click", (event) => { + const button = event.target.closest("button"); + if (button) show(button.getAttribute("data-os")); + }); + + const platform = (navigator.userAgentData && navigator.userAgentData.platform) || navigator.platform || ""; + show(/win/i.test(platform) ? "windows" : "unix"); +}); diff --git a/website/test-install-sh.sh b/website/test-install-sh.sh new file mode 100644 index 00000000..b1435eb0 --- /dev/null +++ b/website/test-install-sh.sh @@ -0,0 +1,68 @@ +#!/bin/sh +# Offline test for ../install.sh: stubs out curl to serve a fake tarball and +# installs into a fake HOME, then checks the PATH setup. Usage: sh test-install-sh.sh +set -eu + +cd "$(dirname "$0")" + +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT INT TERM + +os=$(uname -s) +arch=$(uname -m) +case "$os-$arch" in + Linux-x86_64 | Darwin-arm64) ;; + *) echo "skip: no prebuilt platform for $os/$arch"; exit 0 ;; +esac + +mkdir -p "$tmp/pkg/cx-test" "$tmp/fakebin" +printf '#!/bin/sh\nexit 0\n' > "$tmp/pkg/cx-test/cx" +chmod +x "$tmp/pkg/cx-test/cx" +tar -czf "$tmp/cx.tar.gz" -C "$tmp/pkg" cx-test + +# Stub curl: copies the fake tarball to the -o target, ignoring the URL. +cat > "$tmp/fakebin/curl" < /dev/null +grep -qF "fish_add_path \"$tmp/home2/.cx\"" "$tmp/home2/.config/fish/config.fish" || { echo "FAIL: no fish_add_path"; exit 1; } + +# Unknown shell: prints manual instructions instead. +mkdir -p "$tmp/home3" +out=$(run_install /bin/tcsh "$tmp/home3") +echo "$out" | grep -q "Put $tmp/home3/.cx on your PATH" || { echo "FAIL: no manual fallback: $out"; exit 1; } + +# cx already on PATH: leaves profiles alone. +mkdir -p "$tmp/home4" +PATH="$tmp/home4/.cx:$tmp/fakebin:$PATH" HOME="$tmp/home4" SHELL=/bin/zsh sh ../install.sh > "$tmp/out4" +grep -q "already on your PATH" "$tmp/out4" || { echo "FAIL: no on-PATH shortcut"; exit 1; } +[ ! -e "$tmp/home4/.zshrc" ] || { echo "FAIL: touched .zshrc despite cx on PATH"; exit 1; } + +echo "PASS" diff --git a/website/test-install.mjs b/website/test-install.mjs new file mode 100644 index 00000000..1f3729d8 --- /dev/null +++ b/website/test-install.mjs @@ -0,0 +1,75 @@ +// Exercises website/install.js OS-tab wiring with a minimal fake DOM. +// Run with: node website/test-install.mjs +import { readFile } from "node:fs/promises"; +import vm from "node:vm"; + +const repoRoot = new URL("..", import.meta.url).pathname; +const src = await readFile(repoRoot + "website/install.js", "utf8"); + +let failures = 0; +function check(cond, message) { + console.log((cond ? "PASS: " : "FAIL: ") + message); + if (!cond) failures++; +} + +function runCase(platform) { + const pressed = {}; + const hidden = {}; + const buttons = ["unix", "windows"].map((os) => ({ + getAttribute: (k) => (k === "data-os" ? os : null), + setAttribute: (k, v) => { + if (k === "aria-pressed") pressed[os] = v; + }, + })); + const panes = ["unix", "windows"].map((os) => ({ + getAttribute: (k) => (k === "data-os" ? os : null), + set hidden(v) { + hidden[os] = v; + }, + })); + let clickFn = null; + const tabs = { + querySelectorAll: () => buttons, + addEventListener: (name, fn) => { + clickFn = fn; + }, + }; + const listeners = {}; + const sandbox = { + document: { + querySelector: (sel) => (sel === ".os-tabs" ? tabs : null), + querySelectorAll: (sel) => (sel === ".os-pane" ? panes : []), + addEventListener: (name, fn) => { + listeners[name] = fn; + }, + }, + navigator: { platform, userAgentData: undefined }, + }; + vm.createContext(sandbox); + vm.runInContext(src, sandbox); + return { + pressed, + hidden, + fireLoaded: () => listeners["DOMContentLoaded"](), + click: (os) => clickFn({ target: { closest: () => buttons[os === "unix" ? 0 : 1] } }), + }; +} + +let r = runCase("MacIntel"); +check(Object.keys(r.pressed).length === 0, "nothing happens before DOMContentLoaded"); +r.fireLoaded(); +check(r.pressed.unix === "true" && r.hidden.windows === true, "mac defaults to unix pane"); +r.click("windows"); +check(r.pressed.windows === "true" && r.hidden.unix === true, "manual switch to windows pane"); + +r = runCase("Win32"); +r.fireLoaded(); +check(r.pressed.windows === "true" && r.hidden.unix === true, "windows detected"); +r.click("unix"); +check(r.pressed.unix === "true" && r.hidden.windows === true, "manual override back to unix"); + +if (failures > 0) { + console.error(failures + " install test(s) failed"); + process.exit(1); +} +console.log("All install tests passed."); diff --git a/website/top-nav.html b/website/top-nav.html index f788bb94..704681e1 100644 --- a/website/top-nav.html +++ b/website/top-nav.html @@ -3,6 +3,7 @@ +