Skip to content
Open
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
99 changes: 97 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,79 @@ jobs:
exclude:
- { os: windows, arch: arm64 } # windows-arm64 runner not in free tier
- { compiler: dmd, arch: arm64 } # DMD arm64 still WIP
include:
# GDC: host-only, native arch only (cross promotes to LDC).
- { os: ubuntu, compiler: gdc, arch: x86_64 }
- { os: ubuntu, compiler: gdc, arch: arm64 }
env:
MAKE_OS: ${{ matrix.os == 'ubuntu' && 'linux' || matrix.os }}
steps:
- uses: actions/checkout@v4

- name: Setup D compiler
if: matrix.compiler != 'gdc'
uses: dlang-community/setup-dlang@v2
with:
compiler: ${{ matrix.compiler == 'dmd' && 'dmd-master' || matrix.compiler }}

# TODO: drop the PPA dance and switch to the bare ubuntu-26.04 (and
# ubuntu-26.04-arm) runners once GHA ships them. Tracking issue:
# https://github.com/actions/runner-images/issues (filed 2026-04-24
# "Add Ubuntu 26.04 LTS (Resolute Raccoon)"). Noble (24.04) ships gcc-13;
# numbat (26.04) will ship gcc-16+ which has the DMD frontend 2.112+
# that uRT requires.
#
# gdc-16 lives in ppa:ubuntu-toolchain-r/test (launchpad.net, not
# Azure-mirrored, ~10 min cold install). The .deb files are cached via
# actions/cache so subsequent runs restore from GHA's Azure cache in
# seconds. Bump `gdc-cache-v<N>` below to invalidate the cache when a
# new gdc-16 ships from the PPA.
# Restore now; save unconditionally at the end (build still fails on
# URT-side D-source issues so we need the cache to persist across red
# runs while we chip at them).
- name: Restore gdc-16 .debs
if: matrix.compiler == 'gdc'
id: gdc-cache
uses: actions/cache/restore@v4
with:
path: ~/gdc-debs
key: gdc-16-debs-${{ runner.os }}-${{ runner.arch }}-v1
- name: Download gdc-16 .debs (cache miss)
if: matrix.compiler == 'gdc' && steps.gdc-cache.outputs.cache-hit != 'true'
run: |
sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test
sudo apt-get update
mkdir -p ~/gdc-debs
cd ~/gdc-debs
apt-get download $(apt-cache depends --recurse --no-recommends --no-suggests \
--no-conflicts --no-breaks --no-replaces --no-enhances \
--no-pre-depends gdc-16 | grep "^\w" | sort -u)
- name: Save gdc-16 .debs
if: matrix.compiler == 'gdc' && steps.gdc-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: ~/gdc-debs
key: ${{ steps.gdc-cache.outputs.cache-primary-key }}
- name: Install gdc-16 from .debs
if: matrix.compiler == 'gdc'
run: |
sudo dpkg -i ~/gdc-debs/*.deb || sudo apt-get install -f -y --no-install-recommends
sudo update-alternatives --install /usr/bin/gdc gdc /usr/bin/gdc-16 100
# Bundled druntime/phobos stay on disk: -I src takes priority so
# uRT's core.* / std.* shadows win, and modules we don't shadow
# (e.g. __importc_builtins.d, gcc.*) resolve from the bundled tree.
# We don't link druntime, so unused symbols don't matter.
#
# Exception: `object` is special-cased by GDC (hardcoded druntime
# lookup wins over -I), so the bundled object.d must be removed
# to expose uRT's src/object.d. The same problem hits nested
# modules under std/math/ -- gdc resolves std.math.exponential to
# its bundled file before checking -I src/std/math/exponential.d.
for d in /usr/lib/gcc/*-linux-gnu/16/include/d; do
sudo rm -f "$d/object.d"
sudo rm -rf "$d/std"
done

- name: Install mbedtls (Linux host)
if: matrix.os == 'ubuntu'
run: sudo apt-get update && sudo apt-get install -y libmbedtls-dev
Expand All @@ -47,8 +110,40 @@ jobs:
- name: Build unittest
run: make ARCH=${{ matrix.arch }} OS=${{ env.MAKE_OS }} CONFIG=unittest COMPILER=${{ matrix.compiler }}

- name: Run unittest
run: ./bin/${{ matrix.arch }}_${{ env.MAKE_OS }}_unittest/urt_test${{ matrix.os == 'windows' && '.exe' || '' }}
- name: GDC diagnostics
if: matrix.compiler == 'gdc' && matrix.os == 'ubuntu'
run: |
objdump -d -M intel --disassemble=co_swap \
./bin/${{ matrix.arch }}_${{ env.MAKE_OS }}_unittest/urt_test || true
# Also dump co_switch and the unittest fibre body for the
# urt.fibre crash so we can see the whole call site.
objdump -d -M intel ./bin/${{ matrix.arch }}_${{ env.MAKE_OS }}_unittest/urt_test \
| awk '/<.*co_switch>:/,/^$/' | head -60 || true

- name: Run unittest (Linux, collect core on crash)
if: matrix.os == 'ubuntu'
run: |
sudo apt-get install -y gdb || true
ulimit -c unlimited
sudo sysctl -w kernel.core_pattern=core.%p || true
mkdir -p cores; cd cores
../bin/${{ matrix.arch }}_${{ env.MAKE_OS }}_unittest/urt_test || rc=$?
if [ -n "$rc" ]; then
echo "exit code: $rc"
ls -la
for c in core* /var/lib/apport/coredump/*; do
[ -f "$c" ] || continue
echo "=== $c ==="
gdb -batch -ex 'bt' -ex 'info registers' \
../bin/${{ matrix.arch }}_${{ env.MAKE_OS }}_unittest/urt_test \
"$c" 2>&1 | head -80
done
exit $rc
fi

- name: Run unittest (Windows)
if: matrix.os == 'windows'
run: ./bin/${{ matrix.arch }}_${{ env.MAKE_OS }}_unittest/urt_test.exe

- name: Build release library
if: github.ref == 'refs/heads/release'
Expand Down
68 changes: 67 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
URT_SRCDIR := src

# The first non-pattern target in this Makefile -- needed up-front because
# the GDC block below defines a static rule for the preprocessed mbedtls.i
# that would otherwise become the default goal and short-circuit the build.
.DEFAULT_GOAL := all

include platforms.mk

# =======================================================================
Expand All @@ -19,6 +24,57 @@ include platforms.mk
OBJDIR := obj/$(BUILDNAME)_$(CONFIG)
TARGETDIR := bin/$(BUILDNAME)_$(CONFIG)

# =======================================================================
# GDC ImportC: GDC's ImportC does not preprocess #include directives, so
# any *.c the D side imports (urt.internal.os, urt.internal.mbedtls, ...)
# must be preprocessed to *.i first via the host C compiler. We stage
# them under $(OBJDIR)/imports/ and prepend that to the import path so
# GDC resolves urt.internal.os to os.i instead of os.c.
#
# Two layouts of preprocessed .i:
# * `urt/internal/foo.c` → `imports/urt/internal/foo.i` for files
# consumed via path-based `import urt.internal.foo` (e.g. os.c).
# * `urt/internal/bar.c` → `imports/bar.i` (flat) for files passed as
# sources in URT_SOURCES (e.g. mbedtls.c) -- DMD/LDC's ImportC names
# those by basename, so urt.internal.bar.d is a thin shim that does
# `public import bar;` and stays consistent across all compilers.
# =======================================================================
ifeq ($(COMPILER),gdc)
GDC_I_DIR := $(OBJDIR)/imports
URT_C_SOURCES := $(filter %.c,$(URT_SOURCES))
URT_C_HEADERS := $(filter-out $(URT_C_SOURCES),$(shell find "$(URT_SRCDIR)" -type f -name '*.c' -not -path '$(URT_SRCDIR)/urt/driver/*'))
URT_I_HEADERS := $(patsubst $(URT_SRCDIR)/%.c,$(GDC_I_DIR)/%.i,$(URT_C_HEADERS))
URT_I_SOURCES := $(patsubst %.c,$(GDC_I_DIR)/%.i,$(notdir $(URT_C_SOURCES)))
URT_I_FILES := $(URT_I_HEADERS) $(URT_I_SOURCES)
URT_SOURCES := $(filter-out %.c,$(URT_SOURCES)) $(URT_I_SOURCES)
DFLAGS := -I $(GDC_I_DIR) $(DFLAGS)

# AArch64: GCC's __attribute__((naked)) is silently dropped on aarch64,
# so co_swap can't be inline asm there. Compile src/urt/co_swap.S via host
# gcc and link the .o; fibre.d's aarch64 GNU branch is just an extern decl.
ifeq ($(ARCH),arm64)
URT_S_OBJECTS := $(OBJDIR)/host/co_swap.o
URT_SOURCES := $(URT_SOURCES) $(URT_S_OBJECTS)
endif

$(GDC_I_DIR)/%.i: $(URT_SRCDIR)/%.c
@mkdir -p $(@D)
gcc -E -P -dD $< | \
perl -0777 -pe 's/\b(register|__restrict|__restrict__|__inline__|__inline|__extension__|__signed__|__signed)\b//g; s/__attribute__\s*(\((?:[^()]++|(?1))*\))//g; s/__asm__\s*\(\s*(?:"[^"]*"\s*)+\)//g' \
> $@

# Flat-path rule for URT_SOURCES C files (basename in $(GDC_I_DIR)).
$(GDC_I_DIR)/mbedtls.i: $(URT_SRCDIR)/urt/internal/mbedtls.c
@mkdir -p $(@D)
gcc -E -P -dD $< | \
perl -0777 -pe 's/\b(register|__restrict|__restrict__|__inline__|__inline|__extension__|__signed__|__signed)\b//g; s/__attribute__\s*(\((?:[^()]++|(?1))*\))//g; s/__asm__\s*\(\s*(?:"[^"]*"\s*)+\)//g' \
> $@

$(OBJDIR)/host/%.o: $(URT_SRCDIR)/%.S
@mkdir -p $(@D)
gcc -c -o $@ $<
endif

# Linker script selection for cross-target unittest builds (ESP excluded --
# no ESP-IDF on build slave). platforms.mk falls back to compile-only when
# BAREMETAL_LD isn't set.
Expand Down Expand Up @@ -111,7 +167,10 @@ endif
# Build rule
# =======================================================================

$(TARGET): $(BAREMETAL_OBJS)
.PHONY: all
all: $(TARGET)

$(TARGET): $(BAREMETAL_OBJS) $(URT_I_FILES) $(URT_S_OBJECTS)
mkdir -p $(OBJDIR) $(TARGETDIR)
ifeq ($(COMPILER),ldc)
"$(DC)" $(DFLAGS) $(BUILD_CMD_FLAGS) -of$(TARGET) -od$(OBJDIR) -deps=$(DEPFILE) $(BAREMETAL_OBJS) $(URT_SOURCES)
Expand All @@ -122,6 +181,13 @@ ifeq ($(BUILD_MODE),lib)
else
"$(DC)" $(DFLAGS) $(BUILD_CMD_FLAGS) -of$(TARGET) -od$(OBJDIR) -makedeps $(URT_SOURCES) > $(DEPFILE)
endif
else ifeq ($(COMPILER),gdc)
ifeq ($(BUILD_MODE),lib)
"$(DC)" $(DFLAGS) -c -o $(OBJDIR)/urt.o $(URT_SOURCES)
$(AR) rcs $(TARGET) $(OBJDIR)/urt.o
else
"$(DC)" $(DFLAGS) -o $(TARGET) $(URT_SOURCES)
endif
endif
ifeq ($(BUILD_MODE),embedded-exe)
$(BAREMETAL_OBJCOPY) -O binary $(OBJCOPY_FLAGS) $(TARGET) $(TARGETDIR)/$(TARGETNAME).bin
Expand Down
77 changes: 71 additions & 6 deletions platforms.mk
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,11 @@ endif

# =======================================================================
# Compiler auto-selection: cross-compilation targets use LDC
#
# DMD: only x86/x86_64 host, anything else -> LDC.
# GDC: host-only (any arch with a host toolchain); freertos/baremetal -> LDC.
# Cross GCC toolchains do ship gdc, but wiring those in is a separate
# effort; for now we promote to LDC.
# =======================================================================

ifeq ($(COMPILER),dmd)
Expand All @@ -316,6 +321,12 @@ endif
endif
endif

ifeq ($(COMPILER),gdc)
ifneq ($(filter freertos baremetal,$(OS)),)
COMPILER := ldc
endif
endif

# =======================================================================
# Toolchain discovery (Espressif)
# =======================================================================
Expand All @@ -336,7 +347,8 @@ URT_SOURCES := $(shell find "$(URT_SRCDIR)" -type f -name '*.d' -not -path '$(UR
URT_SOURCES := $(URT_SOURCES) $(shell find "$(URT_SRCDIR)/urt/driver" -maxdepth 1 -type f -name '*.d')
URT_SOURCES := $(URT_SOURCES) $(shell find "$(URT_SRCDIR)/urt/driver/baremetal" -type f -name '*.d')

# mbedtls C glue needs host mbedtls headers -- exclude for embedded targets
# mbedtls C glue needs host mbedtls headers -- exclude for embedded targets.
# urt/internal/os.c already enters via the posix driver dir below.
ifeq ($(filter freertos baremetal,$(OS)),)
URT_SOURCES := $(URT_SOURCES) $(URT_SRCDIR)/urt/internal/mbedtls.c
endif
Expand Down Expand Up @@ -382,7 +394,9 @@ endif
# imports stay in the consumer Makefile.
# =======================================================================

DFLAGS := $(DFLAGS) -preview=bitfields -preview=rvaluerefparam -preview=in #-preview=nosharedaccess <- TODO
# Preview flags translated per-compiler below: dmd/ldc accept -preview=X,
# gdc takes -fpreview=X.
D_PREVIEWS := bitfields rvaluerefparam in #nosharedaccess <- TODO

# OS-level versions
ifeq ($(OS),freertos)
Expand Down Expand Up @@ -467,15 +481,15 @@ else ifeq ($(PLATFORM),esp32-p4)
DFLAGS := $(DFLAGS) -d-version=ESP32_P4
endif

ifeq ($(CONFIG),unittest)
DFLAGS := $(DFLAGS) -unittest
endif

# =======================================================================
# Compiler configuration -- triple, mattr, link flags
# =======================================================================

ifeq ($(COMPILER),ldc)
DFLAGS := $(DFLAGS) $(addprefix -preview=,$(D_PREVIEWS))
ifeq ($(CONFIG),unittest)
DFLAGS := $(DFLAGS) -unittest
endif
# Prefer dlang-installer LDC (avoids system package conflicts with cross-compile)
DC := $(lastword $(sort $(wildcard $(HOME)/dlang/ldc-*/bin/ldc2)))
DC := $(if $(DC),$(DC),ldc2)
Expand Down Expand Up @@ -652,6 +666,11 @@ ifeq ($(COMPILER),ldc)
else ifeq ($(COMPILER),dmd)
DC ?= dmd

DFLAGS := $(DFLAGS) $(addprefix -preview=,$(D_PREVIEWS))
ifeq ($(CONFIG),unittest)
DFLAGS := $(DFLAGS) -unittest
endif

# Strip druntime/phobos, use URT's own object.d.
# Consumers may need to prepend their own -I to shadow druntime's
# __importc_builtins.di (e.g. OpenWatt's third_party/dmd/ for MSVC va_list).
Expand All @@ -670,6 +689,52 @@ else ifeq ($(COMPILER),dmd)
else
DFLAGS := $(DFLAGS) -g -debug
endif

else ifeq ($(COMPILER),gdc)
DC ?= gdc

DFLAGS := $(DFLAGS) $(addprefix -fpreview=,$(D_PREVIEWS))
ifeq ($(CONFIG),unittest)
# URT defines its own extern(C) main in src/urt/package.d, so don't
# pass -fmain (which would emit a duplicate _Dmain).
DFLAGS := $(DFLAGS) -funittest
endif

# Strip druntime/phobos, use URT's own object.d.
# -fno-druntime is too aggressive: it implies -fno-rtti -fno-exceptions
# -fno-moduleinfo, but uRT defines its own TypeInfo (needs RTTI), uses
# try-catch (needs exceptions), and walks ModuleInfo to enumerate
# unittest functions. Pick the subset we actually want:
# -nophoboslib skip linking libgphobos
# GDC's bundled druntime path is shadowed per module under src/core/
# (newaa, array.construction, cast_, ...).
# -fno-omit-frame-pointer: URT's exception unwinder walks the frame chain
# (matches LDC's -frame-pointer=all).
DFLAGS := $(DFLAGS) -nophoboslib -fno-omit-frame-pointer -I $(URT_SRCDIR)

# GDC ignores pragma(lib, "mbedtls") on most binutils setups (.deplibs
# is not honored by ld). Pass explicit -l flags on host posix builds.
# Use -Wl,--no-as-needed so the libs aren't dropped despite appearing
# before the object files that reference them on the link line.
ifneq ($(filter linux ubuntu freebsd,$(OS)),)
DFLAGS := $(DFLAGS) -Wl,--no-as-needed -lmbedtls -lmbedx509 -lmbedcrypto -Wl,--as-needed
endif

ifeq ($(ARCH),x86_64)
DFLAGS := $(DFLAGS) -m64
else ifeq ($(ARCH),x86)
DFLAGS := $(DFLAGS) -m32
else ifeq ($(ARCH),arm64)
# Native arm64 host build -- nothing to add; gdc's default target matches.
else
$(error "GDC: unsupported ARCH=$(ARCH) for PLATFORM=$(PLATFORM) (use COMPILER=ldc)")
endif

ifeq ($(CONFIG),release)
DFLAGS := $(DFLAGS) -O3 -frelease -finline-functions
else
DFLAGS := $(DFLAGS) -g -fdebug
endif
else
$(error "Unknown D compiler: $(COMPILER)")
endif
8 changes: 8 additions & 0 deletions src/core/internal/array/construction.d
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// GDC-side shadow for druntime's core.internal.array.construction.
//
// uRT's src/object.d defines all the array-construction hooks GDC's lowering
// invokes. Re-export the whole `object` module to avoid selective-import
// quirks with extern(C) declarations under GDC.
module core.internal.array.construction;

public import object;
8 changes: 8 additions & 0 deletions src/core/internal/cast_.d
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// GDC-side shadow for druntime's core.internal.cast_.
//
// uRT's src/object.d defines _d_dynamic_cast / _d_interface_cast as extern(C);
// selective named imports of extern(C) symbols across modules don't always
// resolve cleanly under GDC, so re-export the whole `object` module.
module core.internal.cast_;

public import object;
23 changes: 23 additions & 0 deletions src/core/internal/newaa.d
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// GDC-side shadow for druntime's core.internal.newaa.
//
// GDC's bundled object.d always gets compiled (it's load-bearing for
// semantic analysis even with -fno-druntime), and that object.d imports
// AA hooks from core.internal.newaa. With `-I src` ahead of GDC's bundled
// D include path, this shadow wins import resolution and forwards the
// hooks to uRT's template-based AA implementation in urt.internal.aa.
//
// Without this shadow, GDC pulls its bundled core.internal.newaa which
// tries to GC-allocate `Bucket[]` arrays -- incompatible with -fno-druntime.
module core.internal.newaa;

public import urt.internal.aa :
_d_assocarrayliteralTX,
_d_aaNew,
_d_aaLen,
_d_aaGetY,
_d_aaGetRvalueX,
_d_aaIn,
_d_aaDel,
_d_aaApply,
_d_aaApply2,
_d_aaEqual;
Loading
Loading