diff --git a/Cargo.lock b/Cargo.lock index cffea46..331b617 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,6 +112,12 @@ version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.11.1" @@ -220,6 +226,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + [[package]] name = "crc32fast" version = "1.5.0" @@ -783,6 +804,7 @@ dependencies = [ "fs_extra", "humantime", "lsp-types", + "lzma-rs", "regex", "reqwest", "serde", @@ -811,6 +833,16 @@ dependencies = [ "serde_repr", ] +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + [[package]] name = "memchr" version = "2.8.0" diff --git a/Cargo.toml b/Cargo.toml index fb1982a..9282c7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ command-group = "5.0.1" flate2 = "1.1.5" humantime = "2.3.0" lsp-types = "0.97.0" +lzma-rs = "0.3.0" regex = "1.11.1" reqwest = { version = "0.12.24", default-features = false, features = ["blocking", "json", "rustls-tls"] } serde = { version = "1.0.228", features = ["derive"] } diff --git a/activate.sh b/activate.sh index 8e1fe48..bc58a6d 100644 --- a/activate.sh +++ b/activate.sh @@ -1,6 +1,25 @@ # Source this from the repo root to put the tools installed by `make download-dev-env` -# (go, java, node, dotnet) on PATH: `source activate.sh` +# (go, java, node, dotnet, zig, ruby) on PATH: `source activate.sh` case ":$PATH:" in *":$PWD/.env/bin:"*) ;; *) export PATH="$PWD/.env/bin:$PATH" ;; esac + +# ruby-builder's prebuilt ruby needs its lib dir on LD_LIBRARY_PATH (RUNPATH points at the +# hosted-toolcache path it was built for) and RUBYLIB (default $LOAD_PATH is baked in too). +if [[ -d "$PWD/.env/ruby/lib" ]]; then + export LD_LIBRARY_PATH="$PWD/.env/ruby/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" + rubylib_entries=() + for dir in "$PWD"/.env/ruby/lib/ruby/*/; do + [[ -d "$dir" ]] || continue + rubylib_entries+=("${dir%/}") + for arch_dir in "$dir"*linux*/; do + [[ -d "$arch_dir" ]] && rubylib_entries+=("${arch_dir%/}") + done + done + if (( ${#rubylib_entries[@]} > 0 )); then + rubylib_joined=$(IFS=:; echo "${rubylib_entries[*]}") + export RUBYLIB="$rubylib_joined${RUBYLIB:+:$RUBYLIB}" + fi + unset rubylib_entries rubylib_joined dir arch_dir +fi diff --git a/data b/data index 27c97a8..ec28c6b 160000 --- a/data +++ b/data @@ -1 +1 @@ -Subproject commit 27c97a864a4de20181f1098438e1582ffb187b9a +Subproject commit ec28c6bf19fa27df7b4c9451ea299743a98e9bf0 diff --git a/playground/clojure/main.clj b/playground/clojure/main.clj index e69de29..53f7bef 100644 --- a/playground/clojure/main.clj +++ b/playground/clojure/main.clj @@ -0,0 +1,16 @@ +(ns main) + +(defrecord OrderItem [name quantity price]) + +(defrecord Order [customer items]) + +(defn order-total [order] + (reduce + (map (fn [item] (* (:quantity item) (:price item))) (:items order)))) + +(defn build-sample-order [] + (->Order "Carol" [(->OrderItem "Mouse" 1 35.0) (->OrderItem "Pad" 1 12.5)])) + +(defn format-order [order] + (str (:customer order) " has " (count (:items order)) " items worth " (order-total order))) + +(println (format-order (build-sample-order))) diff --git a/playground/luau/main.luau b/playground/luau/main.luau index e69de29..8bc4308 100644 --- a/playground/luau/main.luau +++ b/playground/luau/main.luau @@ -0,0 +1,35 @@ +type OrderItem = { + name: string, + quantity: number, + price: number, +} + +type Order = { + customer: string, + items: { OrderItem }, +} + +local function order_total(order: Order): number + local sum = 0 + for _, item in order.items do + sum += item.quantity * item.price + end + return sum +end + +local function build_sample_order(): Order + return { + customer = "Carol", + items = { + { name = "Mouse", quantity = 1, price = 35.0 }, + { name = "Pad", quantity = 1, price = 12.5 }, + }, + } +end + +local function format_order(order: Order): string + return string.format("%s has %d items worth %.2f", order.customer, #order.items, order_total(order)) +end + +local order = build_sample_order() +print(format_order(order)) diff --git a/playground/odin/main.odin b/playground/odin/main.odin index e69de29..a0b6656 100644 --- a/playground/odin/main.odin +++ b/playground/odin/main.odin @@ -0,0 +1,42 @@ +package main + +import "core:fmt" + +OrderItem :: struct { + name: string, + quantity: int, + price: f64, +} + +Order :: struct { + customer: string, + items: []OrderItem, +} + +order_total :: proc(order: Order) -> f64 { + sum := 0.0 + for item in order.items { + sum += f64(item.quantity) * item.price + } + return sum +} + +build_sample_order :: proc() -> Order { + return Order{ + customer = "Carol", + items = []OrderItem{ + {name = "Mouse", quantity = 1, price = 35.0}, + {name = "Pad", quantity = 1, price = 12.5}, + }, + } +} + +format_order :: proc(order: Order) -> string { + total := order_total(order) + return fmt.tprintf("%s has %d items worth %.2f", order.customer, len(order.items), total) +} + +main :: proc() { + order := build_sample_order() + fmt.println(format_order(order)) +} diff --git a/playground/perl/lib/Order.pm b/playground/perl/lib/Order.pm new file mode 100644 index 0000000..034c750 --- /dev/null +++ b/playground/perl/lib/Order.pm @@ -0,0 +1,18 @@ +package Order; + +use strict; +use warnings; + +sub new { + my ($class, $customer, $items) = @_; + return bless { customer => $customer, items => $items }, $class; +} + +sub total { + my ($self) = @_; + my $sum = 0; + $sum += $_->{quantity} * $_->{price} for @{ $self->{items} }; + return $sum; +} + +1; diff --git a/playground/perl/lib/OrderItem.pm b/playground/perl/lib/OrderItem.pm new file mode 100644 index 0000000..06711ed --- /dev/null +++ b/playground/perl/lib/OrderItem.pm @@ -0,0 +1,11 @@ +package OrderItem; + +use strict; +use warnings; + +sub new { + my ($class, $name, $quantity, $price) = @_; + return bless { name => $name, quantity => $quantity, price => $price }, $class; +} + +1; diff --git a/playground/perl/lib/Report.pm b/playground/perl/lib/Report.pm new file mode 100644 index 0000000..2e3c277 --- /dev/null +++ b/playground/perl/lib/Report.pm @@ -0,0 +1,18 @@ +package Report; + +use strict; +use warnings; +use Exporter qw(import); + +our @EXPORT_OK = qw(format_order); + +sub format_order { + my ($order) = @_; + my $count = scalar @{ $order->{items} }; + return sprintf( + "%s has %d items worth %.2f", + $order->{customer}, $count, $order->total + ); +} + +1; diff --git a/playground/perl/main.pl b/playground/perl/main.pl index e69de29..7319594 100644 --- a/playground/perl/main.pl +++ b/playground/perl/main.pl @@ -0,0 +1,17 @@ +use strict; +use warnings; +use lib 'lib'; +use Order; +use OrderItem; +use Report qw(format_order); + +sub build_sample_order { + my @items = ( + OrderItem->new("Mouse", 1, 35.0), + OrderItem->new("Pad", 1, 12.5), + ); + return Order->new("Carol", \@items); +} + +my $order = build_sample_order(); +print format_order($order), "\n"; diff --git a/playground/ruby/Gemfile b/playground/ruby/Gemfile new file mode 100644 index 0000000..99ae841 --- /dev/null +++ b/playground/ruby/Gemfile @@ -0,0 +1,3 @@ +source "https://rubygems.org" + +ruby ">= 3.0" diff --git a/playground/ruby/lib/order.rb b/playground/ruby/lib/order.rb new file mode 100644 index 0000000..b92ceed --- /dev/null +++ b/playground/ruby/lib/order.rb @@ -0,0 +1,36 @@ +class OrderItem + attr_reader :name, :quantity, :price + + def initialize(name, quantity, price) + @name = name + @quantity = quantity + @price = price + end + + def total + quantity * price + end +end + +class Order + attr_reader :customer, :items + + def initialize(customer, items) + @customer = customer + @items = items + end + + def total + items.sum(&:total) + end +end + +def build_sample_order + Order.new( + "Carol", + [ + OrderItem.new("Mouse", 1, 35.0), + OrderItem.new("Pad", 1, 12.5) + ] + ) +end diff --git a/playground/ruby/lib/report.rb b/playground/ruby/lib/report.rb new file mode 100644 index 0000000..5972d77 --- /dev/null +++ b/playground/ruby/lib/report.rb @@ -0,0 +1,3 @@ +def format_order(order) + "#{order.customer} has #{order.items.length} items worth #{format('%.2f', order.total)}" +end diff --git a/playground/ruby/main.rb b/playground/ruby/main.rb index e69de29..d2f714e 100644 --- a/playground/ruby/main.rb +++ b/playground/ruby/main.rb @@ -0,0 +1,5 @@ +require_relative "lib/order" +require_relative "lib/report" + +order = build_sample_order +puts format_order(order) diff --git a/playground/vim/main.vim b/playground/vim/main.vim index e69de29..693b9de 100644 --- a/playground/vim/main.vim +++ b/playground/vim/main.vim @@ -0,0 +1,14 @@ +function! OrderTotal(quantity, price) + return a:quantity * a:price +endfunction + +function! BuildSampleOrder() + return "Carol" +endfunction + +function! FormatOrder(customer, quantity, price) + let total = OrderTotal(a:quantity, a:price) + return a:customer . " has " . a:quantity . " items worth " . total +endfunction + +echo FormatOrder(BuildSampleOrder(), 2, 35.0) diff --git a/playground/zig/build.zig b/playground/zig/build.zig new file mode 100644 index 0000000..30ee240 --- /dev/null +++ b/playground/zig/build.zig @@ -0,0 +1,16 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const exe = b.addExecutable(.{ + .name = "playground", + .root_module = b.createModule(.{ + .root_source_file = b.path("main.zig"), + .target = target, + .optimize = optimize, + }), + }); + b.installArtifact(exe); +} diff --git a/playground/zig/main.zig b/playground/zig/main.zig index e69de29..f3cb6c7 100644 --- a/playground/zig/main.zig +++ b/playground/zig/main.zig @@ -0,0 +1,49 @@ +const std = @import("std"); + +const OrderItem = struct { + name: []const u8, + quantity: u32, + price: f64, + + fn total(self: OrderItem) f64 { + return @as(f64, @floatFromInt(self.quantity)) * self.price; + } +}; + +const Order = struct { + customer: []const u8, + items: []const OrderItem, + + fn total(self: Order) f64 { + var sum: f64 = 0; + for (self.items) |item| { + sum += item.total(); + } + return sum; + } +}; + +fn sample_order() Order { + return Order{ + .customer = "Carol", + .items = &[_]OrderItem{ + OrderItem{ .name = "Mouse", .quantity = 1, .price = 35.0 }, + OrderItem{ .name = "Pad", .quantity = 1, .price = 12.5 }, + }, + }; +} + +fn format_order(order: Order, buffer: []u8) ![]u8 { + return std.fmt.bufPrint(buffer, "{s} has {d} items worth {d:.2}", .{ + order.customer, + order.items.len, + order.total(), + }); +} + +pub fn main() !void { + const order = sample_order(); + var buffer: [128]u8 = undefined; + const report = try format_order(order, &buffer); + std.debug.print("{s}\n", .{report}); +} diff --git a/scripts/download_dev_env.sh b/scripts/download_dev_env.sh index 9cfdb7d..60ab9d6 100755 --- a/scripts/download_dev_env.sh +++ b/scripts/download_dev_env.sh @@ -1,12 +1,16 @@ #!/usr/bin/env bash # -# Downloads the external runtimes needed by the real-server E2E tests (go, java, node, dotnet) -# into a project-local .env/ directory, so contributors don't need to install them system-wide. +# Downloads the external runtimes needed by the real-server E2E tests (go, java, node, dotnet, +# zig, ruby) into a project-local .env/ directory, so contributors don't need to install them +# system-wide. # # Default versions mirror .github/workflows/ci.yml / e2e.yml (actions/setup-go, # actions/setup-java, actions/setup-node, actions/setup-dotnet). Keep them in sync manually if # CI's pins change; override per-tool via GO_VERSION / JAVA_VERSION / NODE_VERSION / -# DOTNET_CHANNEL. +# DOTNET_CHANNEL / ZIG_VERSION / RUBY_VERSION. +# +# Each runtime's installer lives in scripts/download_dev_env/.inc; add a new one there and +# source+call it below to add another language. # # Usage: scripts/download_dev_env.sh # Then: source activate.sh (from the repo root) @@ -17,10 +21,13 @@ GO_VERSION="${GO_VERSION:-1.27.1}" JAVA_VERSION="${JAVA_VERSION:-21}" NODE_VERSION="${NODE_VERSION:-24}" DOTNET_CHANNEL="${DOTNET_CHANNEL:-10.0}" +ZIG_VERSION="${ZIG_VERSION:-0.15.2}" +RUBY_VERSION="${RUBY_VERSION:-3.3.6}" repo_root=$(git rev-parse --show-toplevel) env_dir="$repo_root/.env" bin_dir="$env_dir/bin" +inc_dir="$repo_root/scripts/download_dev_env" tmp_dir=$(mktemp -d) trap 'rm -rf "$tmp_dir"' EXIT @@ -63,88 +70,19 @@ link() { ln -sf "$target" "$bin_dir/$name" } -install_go() { - local tool_dir="$env_dir/go" - if is_up_to_date "$tool_dir" "$GO_VERSION"; then - echo "go $GO_VERSION already installed, skipping" - return - fi - echo "Installing go $GO_VERSION..." - local archive="$tmp_dir/go.tar.gz" - curl -fsSL -o "$archive" "https://go.dev/dl/go${GO_VERSION}.${os}-${arch/x64/amd64}.tar.gz" - rm -rf "$tool_dir" - mkdir -p "$tool_dir" - tar -xzf "$archive" -C "$tool_dir" --strip-components=1 - echo "$GO_VERSION" > "$(version_stamp "$tool_dir")" - link "$tool_dir/bin/go" go - link "$tool_dir/bin/gofmt" gofmt -} - -install_java() { - local tool_dir="$env_dir/java" - local api_url="https://api.adoptium.net/v3/binary/latest/${JAVA_VERSION}/ga/${os}/${arch}/jdk/hotspot/normal/eclipse" - local resolved_version - resolved_version=$(curl -fsSIL -o /dev/null -w '%{url_effective}' "$api_url") - if is_up_to_date "$tool_dir" "$resolved_version"; then - echo "java (Temurin $JAVA_VERSION) already installed, skipping" - return - fi - echo "Installing java (Temurin $JAVA_VERSION)..." - local archive="$tmp_dir/java.tar.gz" - curl -fsSL -o "$archive" "$api_url" - rm -rf "$tool_dir" - mkdir -p "$tool_dir" - tar -xzf "$archive" -C "$tool_dir" --strip-components=1 - echo "$resolved_version" > "$(version_stamp "$tool_dir")" - link "$tool_dir/bin/java" java - link "$tool_dir/bin/javac" javac -} - -install_node() { - local tool_dir="$env_dir/node" - local index_url="https://nodejs.org/dist/index.json" - local resolved_version - resolved_version=$(curl -fsSL "$index_url" \ - | grep -o "\"version\":\"v${NODE_VERSION}\.[0-9]*\.[0-9]*\"" \ - | head -n1 \ - | sed -E 's/.*"v([0-9.]+)".*/\1/' || true) - if [[ -z "$resolved_version" ]]; then - echo "download-dev-env: could not resolve latest node ${NODE_VERSION}.x from $index_url" >&2 - exit 1 - fi - if is_up_to_date "$tool_dir" "$resolved_version"; then - echo "node $resolved_version already installed, skipping" - return - fi - echo "Installing node $resolved_version..." - local archive="$tmp_dir/node.tar.xz" - curl -fsSL -o "$archive" "https://nodejs.org/dist/v${resolved_version}/node-v${resolved_version}-${os}-${arch}.tar.xz" - rm -rf "$tool_dir" - mkdir -p "$tool_dir" - tar -xJf "$archive" -C "$tool_dir" --strip-components=1 - echo "$resolved_version" > "$(version_stamp "$tool_dir")" - link "$tool_dir/bin/node" node - link "$tool_dir/bin/npm" npm - link "$tool_dir/bin/npx" npx -} - -install_dotnet() { - # dotnet-install.sh is already idempotent: it detects a matching SDK already present in - # --install-dir and skips reinstalling it. - local tool_dir="$env_dir/dotnet" - local install_script="$tmp_dir/dotnet-install.sh" - curl -fsSL -o "$install_script" "https://dot.net/v1/dotnet-install.sh" - chmod +x "$install_script" - echo "Installing dotnet (channel $DOTNET_CHANNEL)..." - mkdir -p "$tool_dir" - "$install_script" --channel "$DOTNET_CHANNEL" --install-dir "$tool_dir" --no-path - link "$tool_dir/dotnet" dotnet -} +source "$inc_dir/go.inc" +source "$inc_dir/java.inc" +source "$inc_dir/node.inc" +source "$inc_dir/dotnet.inc" +source "$inc_dir/zig.inc" +source "$inc_dir/ruby.inc" install_go install_java install_node install_dotnet +install_zig +install_ruby echo echo "Dev env ready under $env_dir" diff --git a/scripts/download_dev_env/dotnet.inc b/scripts/download_dev_env/dotnet.inc new file mode 100644 index 0000000..bec3a83 --- /dev/null +++ b/scripts/download_dev_env/dotnet.inc @@ -0,0 +1,12 @@ +install_dotnet() { + # dotnet-install.sh is already idempotent: it detects a matching SDK already present in + # --install-dir and skips reinstalling it. + local tool_dir="$env_dir/dotnet" + local install_script="$tmp_dir/dotnet-install.sh" + curl -fsSL -o "$install_script" "https://dot.net/v1/dotnet-install.sh" + chmod +x "$install_script" + echo "Installing dotnet (channel $DOTNET_CHANNEL)..." + mkdir -p "$tool_dir" + "$install_script" --channel "$DOTNET_CHANNEL" --install-dir "$tool_dir" --no-path + link "$tool_dir/dotnet" dotnet +} diff --git a/scripts/download_dev_env/go.inc b/scripts/download_dev_env/go.inc new file mode 100644 index 0000000..384818a --- /dev/null +++ b/scripts/download_dev_env/go.inc @@ -0,0 +1,16 @@ +install_go() { + local tool_dir="$env_dir/go" + if is_up_to_date "$tool_dir" "$GO_VERSION"; then + echo "go $GO_VERSION already installed, skipping" + return + fi + echo "Installing go $GO_VERSION..." + local archive="$tmp_dir/go.tar.gz" + curl -fsSL -o "$archive" "https://go.dev/dl/go${GO_VERSION}.${os}-${arch/x64/amd64}.tar.gz" + rm -rf "$tool_dir" + mkdir -p "$tool_dir" + tar -xzf "$archive" -C "$tool_dir" --strip-components=1 + echo "$GO_VERSION" > "$(version_stamp "$tool_dir")" + link "$tool_dir/bin/go" go + link "$tool_dir/bin/gofmt" gofmt +} diff --git a/scripts/download_dev_env/java.inc b/scripts/download_dev_env/java.inc new file mode 100644 index 0000000..cc57729 --- /dev/null +++ b/scripts/download_dev_env/java.inc @@ -0,0 +1,19 @@ +install_java() { + local tool_dir="$env_dir/java" + local api_url="https://api.adoptium.net/v3/binary/latest/${JAVA_VERSION}/ga/${os}/${arch}/jdk/hotspot/normal/eclipse" + local resolved_version + resolved_version=$(curl -fsSIL -o /dev/null -w '%{url_effective}' "$api_url") + if is_up_to_date "$tool_dir" "$resolved_version"; then + echo "java (Temurin $JAVA_VERSION) already installed, skipping" + return + fi + echo "Installing java (Temurin $JAVA_VERSION)..." + local archive="$tmp_dir/java.tar.gz" + curl -fsSL -o "$archive" "$api_url" + rm -rf "$tool_dir" + mkdir -p "$tool_dir" + tar -xzf "$archive" -C "$tool_dir" --strip-components=1 + echo "$resolved_version" > "$(version_stamp "$tool_dir")" + link "$tool_dir/bin/java" java + link "$tool_dir/bin/javac" javac +} diff --git a/scripts/download_dev_env/node.inc b/scripts/download_dev_env/node.inc new file mode 100644 index 0000000..65f7995 --- /dev/null +++ b/scripts/download_dev_env/node.inc @@ -0,0 +1,27 @@ +install_node() { + local tool_dir="$env_dir/node" + local index_url="https://nodejs.org/dist/index.json" + local resolved_version + resolved_version=$(curl -fsSL "$index_url" \ + | grep -o "\"version\":\"v${NODE_VERSION}\.[0-9]*\.[0-9]*\"" \ + | head -n1 \ + | sed -E 's/.*"v([0-9.]+)".*/\1/' || true) + if [[ -z "$resolved_version" ]]; then + echo "download-dev-env: could not resolve latest node ${NODE_VERSION}.x from $index_url" >&2 + exit 1 + fi + if is_up_to_date "$tool_dir" "$resolved_version"; then + echo "node $resolved_version already installed, skipping" + return + fi + echo "Installing node $resolved_version..." + local archive="$tmp_dir/node.tar.xz" + curl -fsSL -o "$archive" "https://nodejs.org/dist/v${resolved_version}/node-v${resolved_version}-${os}-${arch}.tar.xz" + rm -rf "$tool_dir" + mkdir -p "$tool_dir" + tar -xJf "$archive" -C "$tool_dir" --strip-components=1 + echo "$resolved_version" > "$(version_stamp "$tool_dir")" + link "$tool_dir/bin/node" node + link "$tool_dir/bin/npm" npm + link "$tool_dir/bin/npx" npx +} diff --git a/scripts/download_dev_env/ruby.inc b/scripts/download_dev_env/ruby.inc new file mode 100644 index 0000000..efe077f --- /dev/null +++ b/scripts/download_dev_env/ruby.inc @@ -0,0 +1,28 @@ +install_ruby() { + # Prebuilt CRuby from ruby/ruby-builder (same source actions/setup-ruby uses), matched to + # this host's Ubuntu release so the dynamically linked build actually runs. + local tool_dir="$env_dir/ruby" + if is_up_to_date "$tool_dir" "$RUBY_VERSION"; then + echo "ruby $RUBY_VERSION already installed, skipping" + return + fi + local ubuntu_codename + ubuntu_codename=$(. /etc/os-release && echo "$VERSION_ID") + echo "Installing ruby $RUBY_VERSION (ubuntu-${ubuntu_codename})..." + local archive="$tmp_dir/ruby.tar.gz" + curl -fsSL -o "$archive" "https://github.com/ruby/ruby-builder/releases/download/ruby-${RUBY_VERSION}/ruby-${RUBY_VERSION}-ubuntu-${ubuntu_codename}-x64.tar.gz" + rm -rf "$tool_dir" + mkdir -p "$tool_dir" + tar -xzf "$archive" -C "$tool_dir" --strip-components=1 + # ruby-builder bakes its hosted-toolcache build path into every bin/ shebang and into the + # binary's RUNPATH; rewrite the shebangs to this install's real ruby, and rely on + # LD_LIBRARY_PATH (set by the E2E harness / activate.sh) rather than RUNPATH for libruby. + for script in "$tool_dir"/bin/*; do + [[ -f "$script" ]] || continue + sed -i "1s|^#!.*/bin/ruby\$|#!${tool_dir}/bin/ruby|" "$script" + done + echo "$RUBY_VERSION" > "$(version_stamp "$tool_dir")" + link "$tool_dir/bin/ruby" ruby + link "$tool_dir/bin/gem" gem + link "$tool_dir/bin/bundle" bundle +} diff --git a/scripts/download_dev_env/zig.inc b/scripts/download_dev_env/zig.inc new file mode 100644 index 0000000..4abbb76 --- /dev/null +++ b/scripts/download_dev_env/zig.inc @@ -0,0 +1,15 @@ +install_zig() { + local tool_dir="$env_dir/zig" + if is_up_to_date "$tool_dir" "$ZIG_VERSION"; then + echo "zig $ZIG_VERSION already installed, skipping" + return + fi + echo "Installing zig $ZIG_VERSION..." + local archive="$tmp_dir/zig.tar.xz" + curl -fsSL -o "$archive" "https://ziglang.org/download/${ZIG_VERSION}/zig-x86_64-${os}-${ZIG_VERSION}.tar.xz" + rm -rf "$tool_dir" + mkdir -p "$tool_dir" + tar -xJf "$archive" -C "$tool_dir" --strip-components=1 + echo "$ZIG_VERSION" > "$(version_stamp "$tool_dir")" + link "$tool_dir/zig" zig +} diff --git a/src/lsp/client/requests.rs b/src/lsp/client/requests.rs index 32a2efe..9ad8c3c 100644 --- a/src/lsp/client/requests.rs +++ b/src/lsp/client/requests.rs @@ -268,6 +268,8 @@ fn language_id(path: &Path) -> &'static str { Some("py") => "python", Some("rs") => "rust", Some("ts" | "mts" | "cts") => "typescript", + Some("vim") => "vim", + Some("odin") => "odin", _ => "plaintext", } } diff --git a/src/mason/install/artifacts.rs b/src/mason/install/artifacts.rs index b4d18ea..38e940c 100644 --- a/src/mason/install/artifacts.rs +++ b/src/mason/install/artifacts.rs @@ -259,6 +259,8 @@ pub(super) fn install_downloaded_artifact( if relative_name_lower.ends_with(".tar.gz") { extract_tar_gz(root, bytes) + } else if relative_name_lower.ends_with(".tar.xz") { + extract_tar_xz(root, bytes) } else if extension.is_some_and(|value| value.eq_ignore_ascii_case("zip")) { extract_zip(root, bytes) } else if extension.is_some_and(|value| value.eq_ignore_ascii_case("gz")) { @@ -272,6 +274,55 @@ pub(super) fn install_downloaded_artifact( fn extract_tar_gz(root: &Path, bytes: &[u8]) -> Result<()> { let reader = GzDecoder::new(Cursor::new(bytes)); + unpack_tar(root, reader) +} + +fn extract_tar_xz(root: &Path, bytes: &[u8]) -> Result<()> { + let mut decompressed = BoundedWriter::new(MAX_DECOMPRESSED_BYTES); + lzma_rs::xz_decompress(&mut Cursor::new(bytes), &mut decompressed).map_err( + format_root_error("failed to decompress downloaded xz archive in", root), + )?; + unpack_tar(root, Cursor::new(decompressed.into_inner())) +} + +/// A `Write` sink that errors once more than `limit` bytes have been written, guarding xz +/// decompression (which lzma-rs only exposes as "decompress fully into a sink") against +/// decompression bombs the way the streaming tar.gz/zip paths already are per-entry. +struct BoundedWriter { + buffer: Vec, + limit: u64, +} + +impl BoundedWriter { + fn new(limit: u64) -> Self { + Self { + buffer: Vec::new(), + limit, + } + } + + fn into_inner(self) -> Vec { + self.buffer + } +} + +impl std::io::Write for BoundedWriter { + fn write(&mut self, data: &[u8]) -> std::io::Result { + if self.buffer.len() as u64 + data.len() as u64 > self.limit { + return Err(std::io::Error::other( + "decompressed archive exceeds size limit", + )); + } + self.buffer.extend_from_slice(data); + Ok(data.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +fn unpack_tar(root: &Path, reader: impl Read) -> Result<()> { let mut archive = Archive::new(reader); for entry in archive.entries().map_err(format_root_error( "failed to open downloaded tar archive in", diff --git a/tests/e2e/cases/clojure.yaml b/tests/e2e/cases/clojure.yaml index 8feb7d5..d300922 100644 --- a/tests/e2e/cases/clojure.yaml +++ b/tests/e2e/cases/clojure.yaml @@ -1,9 +1,36 @@ language: id: clojure - kind: metadata + kind: source project: playground/clojure + query-profile: + symbol-query: Order + callable-query: build-sample-order + format-file: main.clj + expected-names: + - Order + - OrderItem + - build-sample-order + - format-order pairs: - language: clojure server: clojure_lsp smoke: - status: capabilities + status: queries + exceptions: + - command: grep + outcome: empty-matches + reason: clojure-lsp returns no workspace/symbol matches for this fixture + - command: callers + outcome: empty-matches + reason: build-sample-order is only called from top-level code, not from another named function + - command: declaration + outcome: empty-matches + reason: clojure-lsp returns no textDocument/declaration matches for this fixture + - command: build-index + outcome: failure + message: background-work progress + reason: clojure-lsp exposes no background-work progress signal + lifecycle: + status: scenarios + direct-run: + status: exchange diff --git a/tests/e2e/cases/luau.yaml b/tests/e2e/cases/luau.yaml index e3a1321..89592b5 100644 --- a/tests/e2e/cases/luau.yaml +++ b/tests/e2e/cases/luau.yaml @@ -1,9 +1,30 @@ language: id: luau - kind: metadata + kind: source project: playground/luau + query-profile: + symbol-query: Order + callable-query: build_sample_order + format-file: main.luau + expected-names: + - Order + - OrderItem + - build_sample_order + - format_order pairs: - language: luau server: luau_lsp smoke: - status: capabilities + status: queries + exceptions: + - command: callees + outcome: empty-matches + reason: build_sample_order constructs data directly and has no outgoing call-hierarchy edges + - command: build-index + outcome: failure + message: background-work progress + reason: luau-lsp exposes no background-work progress signal + lifecycle: + status: scenarios + direct-run: + status: exchange diff --git a/tests/e2e/cases/odin.yaml b/tests/e2e/cases/odin.yaml index 6d7d791..75a0653 100644 --- a/tests/e2e/cases/odin.yaml +++ b/tests/e2e/cases/odin.yaml @@ -1,9 +1,30 @@ language: id: odin - kind: metadata + kind: source project: playground/odin + query-profile: + symbol-query: Order + callable-query: build_sample_order + format-file: main.odin + expected-names: + - Order + - OrderItem + - build_sample_order + - format_order pairs: - language: odin server: ols smoke: - status: capabilities + status: queries + exceptions: + - command: grep + outcome: empty-matches + reason: ols returns no workspace/symbol matches for this fixture + - command: build-index + outcome: failure + message: background-work progress + reason: ols exposes no background-work progress signal + lifecycle: + status: scenarios + direct-run: + status: exchange diff --git a/tests/e2e/cases/perl.yaml b/tests/e2e/cases/perl.yaml index 21015c3..ca67c57 100644 --- a/tests/e2e/cases/perl.yaml +++ b/tests/e2e/cases/perl.yaml @@ -1,9 +1,30 @@ language: id: perl - kind: metadata + kind: source project: playground/perl + query-profile: + symbol-query: Order + callable-query: build_sample_order + format-file: main.pl + expected-names: + - Order + - OrderItem + - build_sample_order + - format_order pairs: - language: perl server: perlnavigator smoke: - status: capabilities + status: queries + exceptions: + - command: definition + outcome: empty-matches + reason: perlnavigator does not resolve goto-definition for a sub called from the same file it is defined in + - command: build-index + outcome: failure + message: background-work progress + reason: perlnavigator exposes no background-work progress signal + lifecycle: + status: scenarios + direct-run: + status: exchange diff --git a/tests/e2e/cases/suite.yaml b/tests/e2e/cases/suite.yaml index 2b33e3b..8931389 100644 --- a/tests/e2e/cases/suite.yaml +++ b/tests/e2e/cases/suite.yaml @@ -986,6 +986,19 @@ servers: owner-language: perl provisioning: status: download + host-programs: + - name: node + resolve: + - which + - node + - name: npm + resolve: + - which + - npm + - name: perl + resolve: + - which + - perl - id: perlpls owner-language: perl provisioning: diff --git a/tests/e2e/cases/zig.yaml b/tests/e2e/cases/zig.yaml index efb6e48..a9a3ade 100644 --- a/tests/e2e/cases/zig.yaml +++ b/tests/e2e/cases/zig.yaml @@ -1,9 +1,27 @@ language: id: zig - kind: metadata + kind: source project: playground/zig + query-profile: + symbol-query: Order + callable-query: sample_order + format-file: main.zig + expected-names: + - Order + - OrderItem + - sample_order + - format_order pairs: - language: zig server: zls smoke: - status: capabilities + status: queries + exceptions: + - command: build-index + outcome: failure + message: background-work progress + reason: zls exposes no background-work progress signal + lifecycle: + status: scenarios + direct-run: + status: exchange diff --git a/tests/e2e/harness.rs b/tests/e2e/harness.rs index 9a8edea..b69baf0 100644 --- a/tests/e2e/harness.rs +++ b/tests/e2e/harness.rs @@ -42,6 +42,15 @@ pub(crate) struct E2eContext { // The staged `dotnet` apphost resolves its runtime via DOTNET_ROOT rather than PATH, so its // install root must be threaded through explicitly once `stage_host_program` resolves it. dotnet_root: RefCell>, + // Prebuilt ruby-builder binaries carry a hard-coded RUNPATH and default $LOAD_PATH baked in + // at build time, which don't match wherever this sandbox happens to stage them; + // LD_LIBRARY_PATH/RUBYLIB are the overrides. + ruby_env: RefCell>, +} + +struct RubyEnv { + lib_dir: PathBuf, + rubylib: std::ffi::OsString, } pub(crate) struct E2eOutput { @@ -93,6 +102,7 @@ impl E2eContext { build_dir, data_dir: PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("data"), dotnet_root: RefCell::new(None), + ruby_env: RefCell::new(None), }) } @@ -184,6 +194,32 @@ impl E2eContext { { *self.dotnet_root.borrow_mut() = Some(root.to_path_buf()); } + if name == "ruby" + && let Some(root) = resolved.parent().and_then(Path::parent) + { + let lib_dir = root.join("lib"); + let stdlib_root = lib_dir.join("ruby"); + let mut rubylib_entries = Vec::new(); + if let Ok(versions) = std::fs::read_dir(&stdlib_root) { + for version_dir in versions.flatten().map(|entry| entry.path()) { + rubylib_entries.push(version_dir.clone()); + if let Ok(archs) = std::fs::read_dir(&version_dir) { + rubylib_entries.extend(archs.flatten().map(|entry| entry.path()).filter( + |path| { + path.file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| name.contains("linux")) + }, + )); + } + } + } + if lib_dir.is_dir() && !rubylib_entries.is_empty() { + let rubylib = std::env::join_paths(&rubylib_entries) + .map_err(|error| format!("failed to build RUBYLIB for staged ruby: {error}"))?; + *self.ruby_env.borrow_mut() = Some(RubyEnv { lib_dir, rubylib }); + } + } self.link_host_program(&resolved, &self.bin_dir.join(name)) .map_err(|error| { @@ -272,6 +308,11 @@ impl E2eContext { if let Some(root) = self.dotnet_root.borrow().as_deref() { command.env("DOTNET_ROOT", root); } + if let Some(ruby_env) = self.ruby_env.borrow().as_ref() { + command + .env("LD_LIBRARY_PATH", &ruby_env.lib_dir) + .env("RUBYLIB", &ruby_env.rubylib); + } command }