Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,9 @@ jobs:
# embed rbgo on, and nothing else here would notice (the lanes above run six
# of them). go vet type-checks every package with its tests for the target.
# android and ios link only through cgo, so there the library packages are
# built (no link) instead. plan9 is excluded: its syscall package has no
# errno numbers and no POSIX process or file-lock calls, which rbgo's IO,
# Errno and Process layers are written against (see README "Platforms").
# built (no link) instead. plan9 is vetted like the rest (386, amd64, arm), but
# nothing RUNS there: no CI lane has a Plan 9 kernel, so its tests only
# compile (see README "Platforms" for what works there).
every-target:
name: compile on every target (${{ matrix.goos }})
runs-on: ubuntu-latest
Expand All @@ -148,7 +148,7 @@ jobs:
strategy:
fail-fast: false
matrix:
goos: [aix, android, darwin, dragonfly, freebsd, illumos, ios, js, linux, netbsd, openbsd, solaris, wasip1, windows]
goos: [aix, android, darwin, dragonfly, freebsd, illumos, ios, js, linux, netbsd, openbsd, plan9, solaris, wasip1, windows]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
Expand Down
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -775,15 +775,21 @@ Otherwise:
| **wasip1/wasm** | built with `CGO_ENABLED=0 GOOS=wasip1 GOARCH=wasm` and **run** under [wazero](https://wazero.io), a pure-Go runtime — verified here: `wazero run rbgo.wasm -e 'puts (1..10).sum'` prints `55` |
| **js/wasm (browser)** | `CGO_ENABLED=0 GOOS=js GOARCH=wasm go build ./...` plus `go vet` over `./cmd/wasm ./cmd/rbgo ./internal/vm`, then the playground module is **run** in node by `web/smoke.mjs` (`wasm (js, browser)` in `ci.yml`). Added with #684, which fixed the three `syscall` constants the `js` port does not define |
| **rbgo_core** | `go vet` and the `internal/vm` suite with `-tags rbgo_core`, then the core playground module is built for js/wasm, its size bounded, and **run** in node by `web/smoke.mjs --core` (`rbgo_core (core build)` in `ci.yml`). See [the `rbgo_core` build](#the-rbgo_core-build-the-language-and-the-numeric-stack-no-gems) |
| **every other target** | `go vet ./...`, default and `rbgo_core` builds, for **every GOOS/GOARCH pair `go tool dist list` names except plan9** — aix, android, darwin, dragonfly, freebsd, illumos, ios, js, linux (all 13 arches, mips and ppc64 included), netbsd, openbsd, solaris, wasip1, windows (`compile on every target` in `ci.yml`; android and ios link only through cgo, so their library packages are built instead). Compiling is all it checks there |
| **every other target** | `go vet ./...`, default and `rbgo_core` builds, for **every GOOS/GOARCH pair `go tool dist list` names** — aix, android, darwin, dragonfly, freebsd, illumos, ios, js, linux (all 13 arches, mips and ppc64 included), netbsd, openbsd, plan9 (386, amd64, arm), solaris, wasip1, windows (`compile on every target` in `ci.yml`; android and ios link only through cgo, so their library packages are built instead). Compiling is all it checks there: **no CI lane runs anything on Plan 9**, there being no Plan 9 kernel to run it on |

What a target lacks, rbgo leaves out the way MRI does:

- **The store gems** — SQLite3, Sequel, ActiveRecord's adapter, Bolt, Bleve, Etcd and Confd — need modernc.org/sqlite, which has a port for darwin/{amd64,arm64}, freebsd/{386,amd64,arm,arm64}, linux/{386,amd64,arm,arm64,loong64,ppc64le,riscv64,s390x}, netbsd/amd64, openbsd/{amd64,arm64} and windows. Elsewhere (wasm included) `require` still works and the modules exist, and the first real operation raises `NotImplementedError: sqlite3 is not supported on linux/mips64le` (or the gem and target in question).
- **Errno**: a name the system's `<errno.h>` lacks is still a constant, naming `Errno::NOERROR`, as MRI's `undefined_error` does: `EMULTIHOP` and `ENOLINK` on OpenBSD, `ENOTRECOVERABLE` and `EOWNERDEAD` on NetBSD and DragonFly.
- **`Process::RLIMIT_*`** exist only where the system has the resource: no `RLIMIT_AS` on OpenBSD, no `RLIMIT_MEMLOCK` on AIX, only `RLIMIT_AS` of the four optional ones on Solaris and illumos.
- **`File#flock`** and PStore's lock use `flock(2)`; AIX has none, so there it is emulated with `fcntl(2)` record locks, exactly as MRI's `missing/flock.c` does.
- **plan9 does not compile.** Its `syscall` package has no errno numbers (`syscall.EINVAL` is a string error there), no `Kill`, `Flock`, `Getrlimit` or `Stat_t`, and several gem drivers (nats, Kafka's franz-go, Thrift, gopher-lua) do not build for it; a port means a plan9 variant of rbgo's Errno, IO and Process layers.
- **plan9 (386, amd64, arm) compiles and vets, and is untested at run time.** Its `syscall` package has no errno numbers (`syscall.EINVAL` is a string error there), no `Kill`, `Flock`, `Getrlimit`, `Umask` or `Stat_t`, so rbgo has a plan9 variant of each seam. Nothing has been run on a Plan 9 kernel; the one part with tests is the error-string mapping, which is pure Go and runs on every platform. What that means:
- **Errno**: every `Errno::Exxx` class exists, with the **Linux** number in `Errno::Exxx::Errno` and glibc's wording in the message (MRI's fallback for a name the platform lacks). A failed call's Go error is mapped to a class by the Go standard library's own rules (`syscall.ErrorString.Is`, `os/error_plan9.go`): "file does not exist" is `ENOENT`, "file already exists" `EEXIST`, "permission denied" `EPERM` or `EACCES`, and so on. A string no rule recognises raises plain `SystemCallError` carrying the message.
- **`Process.kill`** works: it writes a note to `/proc/<pid>/note`. `KILL`, `INT`, `HUP`, `ALRM`, `ABRT` and `TERM` (as `interrupt`, like Go's `syscall.SIGTERM`) have notes; other signals, and a negative pid (a process group), raise `Errno::EINVAL`; signal `0` checks `/proc/<pid>` exists. `Process.getrlimit`, `setrlimit`, `getpriority`, `setpriority`, `getpgid`, `getpgrp`, `setpgid`, `setpgrp` and `getsid` raise `NotImplementedError`, and `Process.times` reports no CPU time.
- **`File#flock`** raises `NotImplementedError`, as MRI does where `flock(2)` is missing; PStore works without its cross-process lock. `File.umask` is a no-op reporting `0o022`; `Dir.chroot` raises `Errno::EPERM`.
- **`File::Stat`** comes from `os.FileInfo` plus the Plan 9 `Dir`: `ino` is the Qid path, `dev` packs the server type and subtype, `nlink` is 1, `uid` and `gid` are 0 (owners are names), and `owned?`/`grpowned?` compare the owner and group names with `/dev/user`. `File.lutime` is `File.utime` (Plan 9 has no symlinks).
- **Subprocesses**: a one-string command line goes through `/bin/rc -c`.
- **Gems**: NATS (nats.go uses `syscall.EPIPE`), Kafka (franz-go uses `syscall.ECONNRESET`) and Parquet (apache/thrift uses `syscall.Sockaddr`) do not build for plan9, so `require "nats"`, `"kafka"` and `"parquet"` succeed and the first real operation (`NATS.connect`, `Kafka.new`, `Parquet::ArrowFileReader.new` …) raises `NotImplementedError: nats is not supported on plan9/amd64`. The SQLite3, Sequel, ActiveRecord, Bolt, Bleve and Etcd gems degrade the same way (no modernc.org/sqlite port). `gopher-lua`, which also fails there, is only a test dependency (through `miniredis`), and those tests are skipped on plan9.

**Not on the pull-request path** — validated on a nightly schedule instead:
`riscv64`, `loong64`, `ppc64le`, `s390x` and 32-bit `arm`, under QEMU
Expand Down
2 changes: 1 addition & 1 deletion cmd/rbgo/die_signal_native.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: BSD-3-Clause

//go:build !wasm && !windows
//go:build !wasm && !windows && !plan9

package main

Expand Down
6 changes: 5 additions & 1 deletion cmd/rbgo/die_signal_unsupported.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: BSD-3-Clause

//go:build wasm || windows
//go:build wasm || windows || plan9

package main

Expand All @@ -21,4 +21,8 @@ package main
// program killed by a signal, with the platform-specific part left unclaimed.
// The two end-to-end rows that assert death BY a signal skip on Windows and say
// this is why (cmd/rbgo/exit_status_test.go is POSIX-only for the same reason).
//
// On Plan 9 a process is killed by a NOTE (syscall.Note), not a signal number, and
// there is no 128+signo status to leave behind, so the caller's exit code is all
// that is reported.
func dieBySignal(int) {}
2 changes: 1 addition & 1 deletion cmd/rbgo/exit_status_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: BSD-3-Clause

//go:build !rbgo_closed && !wasm && !windows
//go:build !rbgo_closed && !wasm && !windows && !plan9

package main

Expand Down
51 changes: 51 additions & 0 deletions internal/vm/backends_plan9.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) the go-embedded-ruby/ruby authors
//
// SPDX-License-Identifier: BSD-3-Clause

//go:build plan9 && !rbgo_core

// This file is the GOOS=plan9 counterpart of the gem backends whose Go drivers do
// not compile there: NATS (nats-io/nats.go uses syscall.EPIPE and ECONNRESET),
// Kafka (twmb/franz-go uses syscall.ECONNRESET) and Parquet (apache/arrow-go's
// parquet package pulls apache/thrift, which uses syscall.Sockaddr, Recvfrom and
// EAGAIN). Plan 9's syscall package has no errno numbers and no socket-address
// API, so those packages are compiled out (their files carry !plan9) and the
// bindings degrade the way the store gems do on targets without modernc sqlite
// (bindings_store_stub.go): `require "nats"` still succeeds, the module and its
// exception class exist so a `rescue NATS::Error` loads, and the first real
// operation raises NotImplementedError "<gem> is not supported on plan9/<arch>".

package vm

import "github.com/go-embedded-ruby/ruby/internal/object"

// classOfMsgBinding is the plan9 half of the classOf seam (bindings_msg.go): the
// NATS, Kafka and Parquet value types do not exist here.
func (vm *VM) classOfMsgBinding(v object.Value) (*RClass, bool) { _, _ = vm, v; return nil, false }

// registerNATS installs a graceful NATS module: NATS::Error, and the NATS.connect
// factory plus NATS::Client.new, all raising.
func (vm *VM) registerNATS() {
mod := vm.stubBindingModule("NATS", "NATS::Error")
stubSMethods(mod, "nats", "connect")
client := vm.stubBindingClass(mod, "NATS::Client")
stubSMethods(client, "nats", "new")
}

// registerKafka installs a graceful Kafka module: Kafka::Error, and the Kafka.new
// client factory, which raises.
func (vm *VM) registerKafka() {
mod := vm.stubBindingModule("Kafka", "Kafka::Error")
stubSMethods(mod, "kafka", "new")
}

// registerParquet installs a graceful Parquet module: Parquet::Error, the
// Parquet.load / .save convenience methods and the ArrowFileReader/ArrowFileWriter
// classes whose .new/.open raise.
func (vm *VM) registerParquet() {
mod := vm.stubBindingModule("Parquet", "Parquet::Error")
stubSMethods(mod, "parquet", "load", "save")
for _, n := range []string{"Parquet::ArrowFileReader", "Parquet::ArrowFileWriter"} {
stubSMethods(vm.stubBindingClass(mod, n), "parquet", "new", "open")
}
}
37 changes: 37 additions & 0 deletions internal/vm/bindings_msg.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (c) the go-embedded-ruby/ruby authors
//
// SPDX-License-Identifier: BSD-3-Clause

//go:build !wasm && !plan9 && !rbgo_core

package vm

import "github.com/go-embedded-ruby/ruby/internal/object"

// classOfMsgBinding is the native half of the classOf seam for the NATS, Kafka and
// Parquet bindings, whose Go drivers (nats.go, franz-go, apache/thrift) do not
// build for GOOS=plan9. It handles their value types exactly as the inline cases
// of classOfExtBinding did before they were split out; the plan9 half
// (backends_plan9.go) always reports false, since those types can never be
// constructed there (their register functions are graceful stubs).
func (vm *VM) classOfMsgBinding(v object.Value) (*RClass, bool) {
switch x := v.(type) {
case *NATSX:
return x.cls, true
case *KafkaClient:
return x.cls, true
case *KafkaProducer:
return x.cls, true
case *KafkaConsumer:
return x.cls, true
case *KafkaMessage:
return x.cls, true
case *KafkaBatch:
return x.cls, true
case *ParquetReader:
return vm.consts["Parquet::ArrowFileReader"].(*RClass), true
case *ParquetWriter:
return vm.consts["Parquet::ArrowFileWriter"].(*RClass), true
}
return nil, false
}
21 changes: 5 additions & 16 deletions internal/vm/bindings_native.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,6 @@ func (vm *VM) classOfExtBinding(v object.Value) (*RClass, bool) {
return vm.consts["GRPC::ActiveCall"].(*RClass), true
case *GRPCStatus:
return vm.consts["GRPC::Status"].(*RClass), true
case *NATSX:
return x.cls, true
case *KafkaClient:
return x.cls, true
case *KafkaProducer:
return x.cls, true
case *KafkaConsumer:
return x.cls, true
case *KafkaMessage:
return x.cls, true
case *KafkaBatch:
return x.cls, true
case *MySQLClient:
return x.cls, true
case *MySQLResult:
Expand All @@ -61,10 +49,6 @@ func (vm *VM) classOfExtBinding(v object.Value) (*RClass, bool) {
return x.cls, true
case *MongoObjectId:
return x.cls, true
case *ParquetReader:
return vm.consts["Parquet::ArrowFileReader"].(*RClass), true
case *ParquetWriter:
return vm.consts["Parquet::ArrowFileWriter"].(*RClass), true
case *ArrowArray:
return vm.consts["Arrow::Array"].(*RClass), true
case *ArrowArrayBuilder:
Expand Down Expand Up @@ -102,6 +86,11 @@ func (vm *VM) classOfExtBinding(v object.Value) (*RClass, bool) {
case *OpenStackIdentity:
return vm.consts["OpenStack::Identity"].(*RClass), true
}
// NATS, Kafka and Parquet are a separate seam: their drivers do not build for
// plan9 (see bindings_msg.go and backends_plan9.go).
if c, ok := vm.classOfMsgBinding(v); ok {
return c, true
}
// SQLite3, Sequel, Bolt, Bleve and Etcd are a separate seam: their drivers
// build on fewer targets than the rest (see bindings_store.go).
return vm.classOfStoreBinding(v)
Expand Down
11 changes: 5 additions & 6 deletions internal/vm/dir.go
Original file line number Diff line number Diff line change
Expand Up @@ -591,12 +591,11 @@ func cwdStillNamed(wd string) error {
// not the Errno::ENOENT this used to raise for every failure. Both the class and
// the "chdir_path" label were witnessed against MRI 4.0.5.
func raiseChdirErr(err error, path string) {
var eno syscall.Errno
if errors.As(err, &eno) {
if name, ok := errnoClasses[int64(eno)]; ok {
raise("Errno::"+name, "%s @ chdir_path - %s", errnoStrerror(int64(eno)), path)
if eno, ok := errnoOfError(err); ok {
if name, ok := errnoClasses[eno]; ok {
raise("Errno::"+name, "%s @ chdir_path - %s", errnoStrerror(eno), path)
}
raise("SystemCallError", "%s @ chdir_path - %s", errnoStrerror(int64(eno)), path)
raise("SystemCallError", "%s @ chdir_path - %s", errnoStrerror(eno), path)
}
raise("Errno::ENOENT", "No such file or directory @ chdir_path - %s", path)
}
Expand Down Expand Up @@ -670,7 +669,7 @@ func raiseRmdirErr(err error, path string) {
switch {
case os.IsPermission(err):
raise("Errno::EACCES", "Permission denied @ dir_s_rmdir - %s", path)
case errors.Is(err, syscall.ENOTEMPTY):
case errIsErrno(err, "ENOTEMPTY"):
raise("Errno::ENOTEMPTY", "Directory not empty @ dir_s_rmdir - %s", path)
default:
raise("Errno::ENOENT", "No such file or directory @ dir_s_rmdir - %s", path)
Expand Down
Loading
Loading