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
119 changes: 119 additions & 0 deletions .github/workflows/windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,120 @@ on:
# Build the Aether compiler and ae CLI on Windows using MinGW (GCC).
# MinGW gives us the same GCC/Clang toolchain surface without an MSVC license,
# while our aether_thread.h Win32 compat layer keeps the runtime fully native.
#
# Two tiers (#1592 / #1593):
# 1. windows-cross — a FAST lane on a Linux runner: `zig cc -target
# x86_64-windows-gnu` cross-build of compiler + ae + stdlib, in a few
# minutes. Catches Windows-only COMPILE breakage (missing Win32 shims,
# MinGW-class portability bugs) while the author is still in the PR.
# 2. build-mingw / headers-msvc — the real thing on windows-latest, and
# the only tier that RUNS anything. The actual gate.
#
# The slow tier `needs:` the fast one, so a cross-build failure skips ~20
# minutes of doomed MSYS2 compute. Cost: the MSYS2 legs start a few minutes
# later on green runs — worth it, since a compile break surfaces at minute
# ~4 instead of ~20.
#
# NOT running tests under Wine here is deliberate (#1593 tried it). `ae` is a
# compile-and-run driver: `ae run` under Wine needs a WINDOWS C toolchain
# inside the Wine prefix to compile the C it emits — it tried to download
# 250 MB of MinGW-w64 mid-job. Driving the native `ae` with
# AE_CC="zig cc -target ..." instead needs a Windows libaether.a living
# beside the native one, which one build/ tree cannot hold. Executing tests
# on a Linux runner is therefore a separate piece of work, tracked in its
# own issue; the compile half stands on its own.

jobs:
# ============================================================
# FAST LANE — cross-build for Windows on a Linux runner
# ============================================================
windows-cross:
name: Windows / cross-build (x86_64)
runs-on: ubuntu-latest
timeout-minutes: 25

steps:
- uses: actions/checkout@v4

- name: Install toolchain
run: sudo apt-get update && sudo apt-get install -y gcc make bc curl xz-utils

# Same provisioning + cache discipline as ci.yml's FreeBSD cross leg:
# restore on every run, save only on a main-merge miss. zig bundles the
# mingw-w64 headers + CRT sources, so unlike the FreeBSD leg there is NO
# sysroot to fetch.
- name: Checkout aether-crossbuild
uses: actions/checkout@v4
with:
repository: aether-lang-dev/aether-crossbuild
path: crossbuild
fetch-depth: 1

- name: Restore zig toolchain cache
id: zig_cache
uses: actions/cache/restore@v4
with:
path: |
crossbuild/toolchain
crossbuild/work/downloads
key: zig-only-${{ hashFiles('crossbuild/deps.lock') }}

- name: Provision zig
id: prov
run: |
cd crossbuild
./scripts/get-zig.sh
ZIG="$(find "$PWD/toolchain" -maxdepth 2 -name zig -type f | head -1)"
test -x "$ZIG" || { echo "zig not provisioned"; exit 1; }
echo "zig=$ZIG" >> "$GITHUB_OUTPUT"
"$ZIG" version

- name: Save zig toolchain cache (merge to main, on miss only)
if: >-
github.event_name == 'push' && github.ref == 'refs/heads/main'
&& steps.zig_cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: |
crossbuild/toolchain
crossbuild/work/downloads
key: zig-only-${{ hashFiles('crossbuild/deps.lock') }}

# The fail-fast half: Windows-only -Werror breakage, MinGW __thread
# traps and missing Win32 shims surface here, minutes into the PR.
#
# NB warnings here are NOT fatal (this builds the plain targets, not
# `make ci`). zig's bundled mingw-w64 headers are not byte-identical to
# MSYS2's — e.g. a couple of -Wsign-compare diagnostics originate inside
# Winsock's own FD_SET macro, where SOCKET is unsigned. Promoting those
# to errors would fail the fast lane on a third-party header and train
# people to ignore it. Warning-as-error enforcement stays with the MSYS2
# legs below, against the headers Windows users actually compile with.
- name: Cross-build the toolchain for Windows (WINDOWS=1)
run: |
make compiler ae stdlib \
WINDOWS=1 \
ZIG="${{ steps.prov.outputs.zig }}" \
-j"$(nproc)"

# A cross build that silently emitted Linux binaries would prove nothing
# (mirrors the FreeBSD leg's `file | grep FreeBSD` guard).
- name: Verify the artifacts really are PE
run: |
file build/aetherc.exe build/ae.exe
file build/ae.exe | grep -q 'PE32+' || { echo "ae.exe is not a PE binary"; exit 1; }
file build/aetherc.exe | grep -q 'PE32+' || { echo "aetherc.exe is not a PE binary"; exit 1; }

- name: Upload cross-built binaries
if: always()
uses: actions/upload-artifact@v4
with:
name: aether-windows-cross-x86_64
path: |
build/aetherc.exe
build/ae.exe
retention-days: 7

# ============================================================
# MinGW build — primary Windows target
# Full CI suite: compiler (-Werror), ae, stdlib, unit tests,
Expand All @@ -25,6 +137,12 @@ jobs:
build-mingw:
name: Build + Test (Windows / ${{ matrix.msystem }})
runs-on: windows-latest
# Gate on the fast lane (#1593): a cross-build or Wine failure skips
# ~20 minutes of doomed MSYS2 compute. `needs:`-skipped jobs report as
# SKIPPED, not failed — with these as required checks the PR still
# reads red from windows-cross's own failure, so branch protection
# behaves correctly.
needs: windows-cross

strategy:
fail-fast: false
Expand Down Expand Up @@ -110,6 +228,7 @@ jobs:
headers-msvc:
name: Header compatibility (MSVC)
runs-on: windows-latest
needs: windows-cross

steps:
- name: Checkout repository
Expand Down
50 changes: 50 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`main`, the release pipeline automatically replaces `[current]` with the next
version number before tagging the release.

## [current]

### Added
- `WINDOWS=1` cross-build knob: build the `ae`/`aetherc` toolchain FOR
Windows from a Linux host with `zig cc -target x86_64-windows-gnu`
(#1592). Companion to `FREEBSD=1`, and simpler — zig bundles the
mingw-w64 headers and CRT, so there is no sysroot to fetch.
Capability-lean by design (OpenSSL/zlib/nghttp2/YAML forced off, as
the host's pkg-config would poison the target; vendored PCRE2 keeps
std.regex).
- `AE_TEST_RUNNER`: when set, `ae run` / `ae test` prefix the binary they
just built with that runner instead of exec'ing it directly — the
cargo `CARGO_TARGET_<triple>_RUNNER` pattern (#1592). Empty by
default, so every existing caller is unaffected. Keeps
target-awareness at the edge: nothing in std.spec or the test sources
knows it is running under an emulator.
- CI fast lane `windows-cross` in windows.yml (#1593): cross-builds
compiler + ae + stdlib for Windows on an ubuntu runner and verifies
the artifacts really are PE. The slow MSYS2 legs now `needs:` it, so a
fast-lane failure skips ~20 minutes of doomed compute, and Windows-only
COMPILE breakage surfaces minutes into a PR instead of ~20. It is an
EARLIER signal, never a replacement: MSYS2 remains the fidelity tip and
the only tier that runs anything.
Running the suite under Wine was attempted and deferred: `ae` is a
compile-and-run driver, so `ae run` inside a Wine prefix wants a
Windows C toolchain there to compile the C it emits, and driving the
native `ae` via `AE_CC` needs a Windows `libaether.a` alongside the
native one. The analysis of what such a lane must never claim to cover
is kept in `tests/ae_sweep_prune_wine.txt`.
- `make test-ae` gained `AE_SWEEP_EXTRA_PRUNE=<file>`, layering an extra
exclusion list over `tests/ae_sweep_prune.txt` — applied to both the
`.ae` and the `.sh` sweeps (the latter previously had no prune filter
at all).
- A build-target stamp (`build/.build-target`): a native build after a
cross build (or vice versa) now fails immediately with an actionable
message instead of dying deep in the link with "unknown file type" —
object and archive formats do not mix in one `build/` tree. `make
clean` / `make help` are exempt so the guard never blocks its own
remedy.

### Fixed
- README's "enforced from compile time down to libc" now names the
platform boundary: the libc (LD_PRELOAD) tier is Linux/FreeBSD only,
and Windows gets the compile-time and scope layers (#1594).
docs/containment-sandbox.md states that Windows is out *by
construction* rather than by backlog, and that running under Wine
does not exercise containment at all (preloading into the wine
process intercepts wine's libc calls, not the guest's) — so a green
Wine run must never be read as containment coverage.

## [0.541.0]

### Fixed
Expand Down
114 changes: 112 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@
# IS_WINDOWS is set for any Windows variant (native, MSYS2, MinGW, Cygwin).
WINDOWS_NATIVE :=
IS_WINDOWS :=
# WINDOWS=1 — cross-build FOR Windows from a non-Windows host (see the
# "Windows cross-build" block below). Declared here, ahead of host
# detection, because every downstream conditional keys off IS_WINDOWS /
# EXE_EXT: the cross build wants the Windows CODE PATHS with a Linux
# host's shell utilities, so it sets those two and leaves WINDOWS_NATIVE
# (which switches to cmd.exe-isms like `if not exist`) unset.
ifeq ($(WINDOWS),1)
IS_WINDOWS := 1
EXE_EXT := .exe
DETECTED_OS := MINGW64_NT-cross
endif
ifeq ($(OS),Windows_NT)
IS_WINDOWS := 1
_UNAME_S := $(shell uname -s 2>&1)
Expand All @@ -29,7 +40,7 @@ ifeq ($(OS),Windows_NT)
EXE_EXT := .exe
WINDOWS_NATIVE := 1
endif
else
else ifneq ($(WINDOWS),1)
DETECTED_OS := $(shell uname -s)
ifneq ($(findstring MINGW,$(DETECTED_OS)),)
EXE_EXT := .exe
Expand Down Expand Up @@ -150,6 +161,8 @@ $(VERSION_HEADER): VERSION Makefile
mv "$$tmp" "$(VERSION_HEADER)"; \
echo "Generated $(VERSION_HEADER) (v$(VERSION))"; \
fi
@mkdir -p build 2>/dev/null || true
@echo "$(BUILD_TARGET_ID)" > "$(BUILD_TARGET_STAMP)"

# Convenience phony alias for explicit regeneration (e.g. `make gen-version-header`).
.PHONY: gen-version-header
Expand Down Expand Up @@ -271,6 +284,86 @@ ifeq ($(FREEBSD),1)
-L$(AETHER_SYSROOT)/usr/lib -L$(AETHER_SYSROOT)/lib
endif

# ---------------------------------------------------------------------------
# Windows cross-build (WINDOWS=1) — build the ae/aetherc toolchain FOR Windows
# from a Linux (or any) host, via `zig cc -target x86_64-windows-gnu` (#1592).
# Companion to FREEBSD=1 above, and deliberately simpler: zig BUNDLES the
# mingw-w64 headers and CRT sources and builds them on demand, so unlike the
# FreeBSD leg there is NO sysroot to fetch and no -nostdlib CRT bookkeeping.
#
# Required input:
# ZIG path to a zig binary (>= 0.13) e.g. .../zig
# Provision with the aether-crossbuild repo's get-zig.sh (same as FreeBSD).
#
# Purpose is a FAST Windows signal on a Linux runner (#1593): the MSYS2 legs
# take ~20 minutes, so Windows-specific compile breakage is discovered long
# after the author moved on. This lane compiles the same code
# paths in a few minutes. It is an EARLIER signal, never a replacement — the
# MSYS2 jobs stay the fidelity tip.
#
# Capability-lean by design: the optional Tier-2 libs are forced OFF, exactly
# as the FreeBSD cross does and for the same reason — host pkg-config would
# find the host's Linux libs and poison the Windows binary. Cross-compiling
# OpenSSL/nghttp2 for windows-gnu is the genuinely expensive part and is out
# of scope; std ships those features as their "unavailable" stubs. Vendored
# PCRE2 needs no host library, so std.regex stays available.
WINDOWS_CPU ?= x86_64
ifeq ($(WINDOWS),1)
ifeq ($(ZIG),)
$(error WINDOWS=1 needs ZIG=<path to zig> (>= 0.13))
endif
CC := $(ZIG) cc -target $(WINDOWS_CPU)-windows-gnu
AR := $(ZIG) ar
RANLIB := $(ZIG) ranlib
# Host pkg-config is wrong for the target — force the optional libs off.
OPENSSL := 0
ZLIB := 0
NGHTTP2 := 0
YAML := 0
# PCRE2 has a vendored in-tree fallback (no host library involved), so
# std.regex survives the cross build.
PCRE2 := vendored
# NB __USE_MINGW_ANSI_STDIO=1 still applies (it keys off IS_WINDOWS
# below): zig ships mingw-w64's libmingwex, so the __mingw_* printf
# family resolves exactly as it does under MSYS2. Verified by a clean
# cross-build + link of compiler/ae/stdlib.
endif

# A cross build OWNS build/ for the duration: build/obj holds PE objects and
# build/libaether*.a become Windows archives, so a native build afterwards
# links host code against Windows objects and dies deep in the link with
# confusing errors. Rather than fork the whole tree layout (dozens of rules
# reference build/libaether.a by literal path), stamp the tree with the
# target it was last built for and fail loudly at the START of the next
# mismatching build. `make clean` between targets is the fix; CI runners
# build one target per job and never hit it.
# Literal `build/` (not $(BUILD_DIR)): this block runs ahead of the
# BUILD_DIR assignment further down, and the version-header rule that
# writes the stamp is earlier still.
BUILD_TARGET_STAMP := build/.build-target
ifeq ($(WINDOWS),1)
BUILD_TARGET_ID := windows-$(WINDOWS_CPU)
else ifeq ($(FREEBSD),1)
BUILD_TARGET_ID := freebsd-$(FREEBSD_CPU)
else
BUILD_TARGET_ID := native-$(DETECTED_OS)
endif
_STAMPED := $(strip $(shell cat $(BUILD_TARGET_STAMP) 2>/dev/null))
# The check runs at PARSE time, so it must not fire for the very goals that
# resolve it: `make clean` (and clean-ish goals) would otherwise be
# unreachable — the guard would block the only way out of the state it is
# complaining about. $(MAKECMDGOALS) is empty for a bare `make` (== all).
_CLEAN_ONLY := $(if $(strip $(filter-out clean distclean help,$(or $(MAKECMDGOALS),all))),,1)
ifneq ($(_STAMPED),)
ifneq ($(_STAMPED),$(BUILD_TARGET_ID))
ifneq ($(_CLEAN_ONLY),1)
$(error build/ holds $(_STAMPED) artifacts but this is a $(BUILD_TARGET_ID) build. \
Object and archive formats do not mix — run `make clean` first (or build the \
other target in a separate checkout))
endif
endif
endif

# MinGW / MSYS2 (native Windows): bind the printf family to the
# C99-conformant __mingw_* implementations instead of legacy MSVCRT.
# MSVCRT mishandles the C99 conversions we emit (%lld / %llu / %zu / %g)
Expand Down Expand Up @@ -478,7 +571,12 @@ endif
# only the loader/threads/math libs are needed at link time; macOS needs the
# audio frameworks. Windows folds its audio into the existing WIN_LINK_LIBs.
AUDIO_LDFLAGS :=
ifeq ($(shell uname -s),Linux)
# Keyed on the HOST uname, so a cross build must opt out explicitly or it
# picks up the host's POSIX libs (zig-lld: "unable to find dynamic system
# library 'pthread'"). Windows folds audio into WIN_LINK_LIBS either way.
ifeq ($(WINDOWS),1)
AUDIO_LDFLAGS :=
else ifeq ($(shell uname -s),Linux)
AUDIO_LDFLAGS := -lpthread -ldl -lm
else ifeq ($(shell uname -s),FreeBSD)
AUDIO_LDFLAGS := -lpthread -lm
Expand Down Expand Up @@ -534,6 +632,7 @@ ifeq ($(HARDEN),1)
CFLAGS += -fstack-protector-all -D_FORTIFY_SOURCE=2 -Wformat -Wformat-security
endif
ifneq ($(FREEBSD),1)
ifneq ($(WINDOWS),1)
ifneq ($(PLATFORM),wasm)
ifneq ($(PLATFORM),embedded)
ifeq ($(findstring AETHER_NO_THREADING,$(EXTRA_CFLAGS)),)
Expand All @@ -542,6 +641,7 @@ endif
endif
endif
endif
endif

# Zero warnings achieved - ready for -Werror
BUILD_DIR = build
Expand Down Expand Up @@ -1045,6 +1145,10 @@ test-ae: compiler ae stdlib
chmod +x "$$script"; \
root=$$(pwd); \
sed '/^#/d' tests/ae_sweep_prune.txt > "$$tmpdir/prune.txt"; \
if [ -n "$(AE_SWEEP_EXTRA_PRUNE)" ]; then \
sed '/^#/d;/^$$/d' "$(AE_SWEEP_EXTRA_PRUNE)" >> "$$tmpdir/prune.txt"; \
echo " Extra prune list: $(AE_SWEEP_EXTRA_PRUNE) ($$(sed '/^#/d;/^$$/d' "$(AE_SWEEP_EXTRA_PRUNE)" | wc -l | tr -d ' ') patterns)"; \
fi; \
find tests/syntax tests/compiler tests/integration tests/regression -name '*.ae' -print 2>/dev/null \
| grep -v -F -f "$$tmpdir/prune.txt" | sort | \
xargs -P $(NPROC) -I{} "$$script" "{}" "$$tmpdir" "$$root"; \
Expand Down Expand Up @@ -1076,7 +1180,13 @@ test-ae: compiler ae stdlib
printf 'done\n' >> "$$sh_script"; \
chmod +x "$$sh_script"; \
sh_nproc=$${SH_NPROC:-1}; \
if [ -n "$(AE_SWEEP_EXTRA_PRUNE)" ]; then \
sed '/^#/d;/^$$/d' "$(AE_SWEEP_EXTRA_PRUNE)" > "$$tmpdir/shprune.txt"; \
else \
: > "$$tmpdir/shprune.txt"; \
fi; \
find tests/integration -name 'test_*.sh' 2>/dev/null | xargs -n1 dirname | sort -u \
| { if [ -s "$$tmpdir/shprune.txt" ]; then grep -v -F -f "$$tmpdir/shprune.txt"; else cat; fi; } \
| xargs -P $$sh_nproc -I{} "$$sh_script" "{}" "$$tmpdir"; \
passed=$$(ls "$$tmpdir"/PASS_* 2>/dev/null | wc -l | tr -d ' '); \
failed=$$(ls "$$tmpdir"/FAIL_* 2>/dev/null | wc -l | tr -d ' '); \
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS%20%7C%20WASM%20%7C%20Embedded-lightgrey)]()
[![Website](https://img.shields.io/badge/website-aether--lang.dev-8A2BE2)](https://aether-lang.dev/)

A compiled actor language whose permissions are part of the source, enforced from compile time down to libc. No VM and no garbage collector: the compiler emits readable C.
A compiled actor language whose permissions are part of the source, enforced from compile time down to libc (the libc layer on Linux/FreeBSD; Windows gets the compile-time and scope layers). No VM and no garbage collector: the compiler emits readable C.

**Website: [aether-lang.dev](https://aether-lang.dev/)**

## Overview

Most languages treat "what may this program touch?" as a deployment problem. Aether makes it a language problem: code runs against an explicit grant list, enforced three times over. At compile time, `--emit=lib` starts capability-empty and the host opts modules in with `--with=fs,net,os`. At scope level, `hide` and `seal except` stop ambient names from leaking into any lexical block, a closure, a trailing-block DSL, an actor handler. At runtime, an `LD_PRELOAD` shim checks libc itself (`open*`, `connect`/`bind`, `execve`, `mmap`, `dlopen`, `getenv`) against the same grants, inherited across `execve`. See [Containment Sandbox](docs/containment-sandbox.md) for the threat model, the prior art it draws on, and the known bypass surface.
Most languages treat "what may this program touch?" as a deployment problem. Aether makes it a language problem: code runs against an explicit grant list, enforced three times over. At compile time, `--emit=lib` starts capability-empty and the host opts modules in with `--with=fs,net,os`. At scope level, `hide` and `seal except` stop ambient names from leaking into any lexical block, a closure, a trailing-block DSL, an actor handler. At runtime, an `LD_PRELOAD` shim checks libc itself (`open*`, `connect`/`bind`, `execve`, `mmap`, `dlopen`, `getenv`) against the same grants, inherited across `execve` — this third layer is Linux/FreeBSD only (there is no `LD_PRELOAD` on Windows). See [Containment Sandbox](docs/containment-sandbox.md) for the threat model, the prior art it draws on, and the known bypass surface.

Concurrency is actor-shaped: `actor`, `receive` and `!`, with automatic multi-core scheduling, lock-free mailboxes, and migration that converges chatty actors onto one core. The compiler emits readable C, not bytecode and not a VM, which is an implementation choice rather than the pitch: it buys native speed, direct linking against existing C libraries, and a runtime that ports anywhere a C toolchain reaches.

Expand Down
Loading
Loading