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
150 changes: 141 additions & 9 deletions .github/workflows/release-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,28 +50,121 @@ jobs:
arimo-bootstrap/target
key: linux-v1-${{ hashFiles('arimo-bootstrap/Cargo.lock') }}

# ── Stage 0: bootstrap compiler ──────────────────────────────────────

- name: Build Stage 0 (bootstrap)
working-directory: arimo-bootstrap
run: cargo build --release --target x86_64-unknown-linux-gnu

- name: Build Stage 1 (self-hosted Linux)
# ── Stage 1: S1 → S2 (bootstrap compiles arc source) ─────────────────

- name: Build S2 (bootstrap → self-hosted)
working-directory: arimo
run: |
ARC0="../arimo-bootstrap/target/x86_64-unknown-linux-gnu/release/arc"
chmod +x "$ARC0"
cp -r stdlib "../arimo-bootstrap/target/x86_64-unknown-linux-gnu/release/"
"$ARC0" build --target linux
chmod +x arc
cp arc ../arc-linux
cp arc /tmp/arc.s2
echo "S2 size: $(stat -c%s /tmp/arc.s2) bytes"

# ── Stage 2: S2 → S3 (self-hosted compiles itself) ───────────────────

- name: Build S3 (self-hosted → self-hosted)
working-directory: arimo
run: |
rm -f arc
/tmp/arc.s2 build --target linux
chmod +x arc
cp arc /tmp/arc.s3
echo "S3 size: $(stat -c%s /tmp/arc.s3) bytes"

- name: Run test suite (smoke test)
# ── Stage 3: S3 → S4 (verify determinism) ────────────────────────────

- name: Build S4 (determinism check)
working-directory: arimo
run: |
rm -f arc
/tmp/arc.s3 build --target linux
chmod +x arc
cp arc /tmp/arc.s4
echo "S4 size: $(stat -c%s /tmp/arc.s4) bytes"

- name: Verify S3 == S4 (determinism)
run: |
diff /tmp/arc.s3 /tmp/arc.s4 && echo "✅ S3 == S4 DETERMINISTIC" || {
echo "❌ FAIL: S3 ≠ S4 — non-deterministic output"
echo "S3 SHA256: $(sha256sum /tmp/arc.s3 | awk '{print $1}')"
echo "S4 SHA256: $(sha256sum /tmp/arc.s4 | awk '{print $1}')"
exit 1
}

# ── Verify S3 is static + self-contained ─────────────────────────────

- name: Verify static binary (no libc dependency)
run: |
RELEASE_BIN=/tmp/arc.s3
echo "=== file ==="
file "$RELEASE_BIN"

echo "=== ldd ==="
if ldd "$RELEASE_BIN" 2>&1 | grep -q "not a dynamic executable"; then
echo "✅ ldd: not a dynamic executable"
elif ldd "$RELEASE_BIN" 2>&1 | grep -q "statically linked"; then
echo "✅ ldd: statically linked"
else
LDD_OUT=$(ldd "$RELEASE_BIN" 2>&1)
if echo "$LDD_OUT" | grep -qE "libc\.|libpthread|libm\.|libdl\.|ld-linux"; then
echo "❌ FAIL: binary has dynamic library dependencies"
echo "$LDD_OUT"
exit 1
fi
echo "ldd output: $LDD_OUT"
fi

echo "=== readelf -l (check for INTERP segment) ==="
if readelf -l "$RELEASE_BIN" 2>/dev/null | grep -q "INTERP"; then
echo "❌ FAIL: binary has INTERP segment (dynamic linker required)"
readelf -l "$RELEASE_BIN" | grep "INTERP\|LOAD\|DYNAMIC"
exit 1
else
echo "✅ No INTERP segment — static binary"
fi

echo "=== readelf -d (check for dynamic section) ==="
if readelf -d "$RELEASE_BIN" 2>/dev/null | grep -q "NEEDED"; then
echo "❌ FAIL: binary has NEEDED entries (shared library dependencies)"
readelf -d "$RELEASE_BIN"
exit 1
else
echo "✅ No NEEDED entries — self-contained"
fi

echo ""
echo "=== Static verification PASSED ==="
echo "Release binary: $(stat -c%s "$RELEASE_BIN") bytes"
echo "SHA256: $(sha256sum "$RELEASE_BIN" | awk '{print $1}')"

# ── Smoke test ───────────────────────────────────────────────────────

- name: Smoke test (compile hello.arm)
working-directory: arimo/test
run: |
ARC="/home/runner/work/arimo/arimo/arimo/arc"
RELEASE_BIN=/tmp/arc.s3
cp -r ../stdlib /tmp/
"$ARC" hello.arm --target linux 2>&1 | tail -1
chmod +x hello && ./hello
echo "Smoke test passed"
"$RELEASE_BIN" hello.arm --target linux 2>&1
if [ -f hello ]; then
chmod +x hello
OUTPUT=$(./hello 2>&1)
echo "hello output: $OUTPUT"
echo "✅ Smoke test passed"
else
echo "❌ Smoke test failed: hello binary not produced"
exit 1
fi

# ── Package ──────────────────────────────────────────────────────────

- name: Install packaging tools
run: |
Expand All @@ -83,9 +176,10 @@ jobs:
run: |
TAG="${{ github.event.inputs.tag || github.ref_name }}"
VERSION="${TAG#v}"
RELEASE_BIN=/tmp/arc.s3

mkdir -p dist/arimo-linux-x64/stdlib
cp arc dist/arimo-linux-x64/
cp "$RELEASE_BIN" dist/arimo-linux-x64/arc
cp -r stdlib/. dist/arimo-linux-x64/stdlib/

cat > dist/arimo-linux-x64/install.sh << 'INSTALL'
Expand All @@ -109,7 +203,7 @@ jobs:

mkdir -p pkg-root/usr/local/bin
mkdir -p pkg-root/usr/local/lib/arimo/stdlib
cp arc pkg-root/usr/local/bin/arc
cp "$RELEASE_BIN" pkg-root/usr/local/bin/arc
cp -r stdlib/. pkg-root/usr/local/lib/arimo/stdlib/

fpm -s dir -t deb -n arimo -v "${VERSION}" --architecture amd64 \
Expand All @@ -126,6 +220,44 @@ jobs:
--license "AGPL-3.0" --prefix / -C pkg-root \
-p "dist/arimo-${TAG}-linux-x64.rpm" .

echo "=== Packaged files ==="
ls -la dist/

# ── Verify packaged binary is static ─────────────────────────────────

- name: Verify packaged tar.gz contains static binary
run: |
TAG="${{ github.event.inputs.tag || github.ref_name }}"
TARBALL="arimo/dist/arimo-${TAG}-linux-x64.tar.gz"

mkdir -p /tmp/verify-release
tar xzf "$TARBALL" -C /tmp/verify-release

PACKAGED_BIN=/tmp/verify-release/arimo-linux-x64/arc

echo "=== Verifying packaged binary ==="
file "$PACKAGED_BIN"

if file "$PACKAGED_BIN" | grep -q "dynamically linked"; then
echo "❌ FAIL: Packaged binary is dynamically linked!"
exit 1
fi

if readelf -l "$PACKAGED_BIN" 2>/dev/null | grep -q "INTERP"; then
echo "❌ FAIL: Packaged binary has INTERP segment!"
exit 1
fi

ldd "$PACKAGED_BIN" 2>&1 | grep -q "not a dynamic executable" || {
echo "❌ FAIL: Packaged binary has dynamic dependencies!"
exit 1
}

echo "✅ Packaged binary is static and self-contained"
sha256sum "$PACKAGED_BIN"

# ── Upload ───────────────────────────────────────────────────────────

- name: Upload to GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand Down
28 changes: 28 additions & 0 deletions .github/workflows/test-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,17 +97,45 @@ jobs:
run: |
cp arc /tmp/arc.s1
echo "=== S1 binary: $(stat -c%s /tmp/arc.s1) bytes ==="
file /tmp/arc.s1
echo "=== S1→S2 build (bootstrap-built → self-hosted) ==="
/tmp/arc.s1 build --target linux 2>&1
cp arc /tmp/arc.s2
echo "=== S2 binary: $(stat -c%s /tmp/arc.s2) bytes ==="
file /tmp/arc.s2
echo "=== S2→S3 build ==="
/tmp/arc.s2 build --target linux 2>&1
cp arc /tmp/arc.s3
echo "=== S3 binary: $(stat -c%s /tmp/arc.s3) bytes ==="
file /tmp/arc.s3
diff /tmp/arc.s2 /tmp/arc.s3 && echo "S2==S3 DETERMINISTIC" || echo "S2≠S3 (bootstrap→self-hosted — expected)"
echo "=== S3→S4 build (second self-hosted) ==="
/tmp/arc.s3 build --target linux 2>&1
cp arc /tmp/arc.s4
echo "=== S4 binary: $(stat -c%s /tmp/arc.s4) bytes ==="
file /tmp/arc.s4
diff /tmp/arc.s3 /tmp/arc.s4 && echo "S3==S4 DETERMINISTIC ✓" || { echo "FAIL: S3≠S4 non-deterministic"; exit 1; }

- name: Verify S3 is static (self-hosted = no libc dependency)
run: |
S3=/tmp/arc.s3
echo "=== file ==="
file "$S3"
echo "=== ldd ==="
ldd "$S3" 2>&1 || true
echo "=== readelf -l ==="
readelf -l "$S3" 2>/dev/null | grep -E "INTERP|LOAD|DYNAMIC" || true

echo ""
if file "$S3" | grep -q "dynamically linked"; then
echo "WARN: S3 is dynamically linked (bootstrap codegen via gcc)"
echo "This is expected for S1/S2 but self-hosted S3+ should be static."
elif file "$S3" | grep -q "statically linked"; then
echo "✅ S3 is statically linked — self-hosted native binary"
fi

if readelf -l "$S3" 2>/dev/null | grep -q "INTERP"; then
echo "WARN: S3 has INTERP segment (dynamic linker)"
else
echo "✅ S3 has no INTERP segment — no dynamic linker dependency"
fi
70 changes: 54 additions & 16 deletions arimo/compiler/backend/ELFWriter.arm
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,30 @@ extern "C" {
}

public class ELFWriter {
private enc : X64Encoder;
private strNames : List<String>;
private strConts : List<String>;
private enc : X64Encoder;
private strNames : List<String>;
private strConts : List<String>;
private bssLabels : List<String>;
private bssSizes : List<Integer>;

public constructor(enc: X64Encoder) {
this.enc = enc;
this.strNames = List();
this.strConts = List();
this.enc = enc;
this.strNames = List();
this.strConts = List();
this.bssLabels = List();
this.bssSizes = List();
}

public addString(name: String, content: String) {
this.strNames.append(name);
this.strConts.append(content);
}

public addBss(name: String, size: Integer) {
this.bssLabels.append(name);
this.bssSizes.append(size);
}

private alignTo(v: Integer, a: Integer) : Integer {
Integer rem = v % a;
if (rem == 0) { return v; }
Expand Down Expand Up @@ -114,8 +123,23 @@ public class ELFWriter {
strCursor = strCursor + c.length() + 1;
si = si + 1;
}
Integer dataSize = strCursor;
if (dataSize == 0) { dataSize = 8; }
// Align data cursor to 8 bytes for BSS globals
Integer dataCursor = strCursor;
if (dataCursor == 0) { dataCursor = 8; }
Integer dataAligned = this.alignTo(dataCursor, 8);

// Precompute BSS offsets (relative to start of .data segment)
List<Integer> bssOffsets = List();
Integer bssCursor = dataAligned - strCursor; // offset from string end
Integer bi = 0;
while (bi < this.bssLabels.length()) {
bssOffsets.append(dataAligned + bi * 8); // each BSS slot 8-byte aligned
bi = bi + 1;
}
Integer bssTotal = this.bssLabels.length() * 8;
Integer dataFileSize = dataAligned; // what goes in the file
Integer dataMemSize = dataAligned + bssTotal; // virtual size includes BSS
if (dataMemSize == 0) { dataFileSize = 8; dataMemSize = 8; }

Integer textPadded = this.alignTo(codeSize, pageSize);
Integer dataFileOff = textFileOff + textPadded;
Expand All @@ -125,27 +149,37 @@ public class ELFWriter {
// Apply intra-.text jump fixups
this.enc.applyFixups();

// Apply RIP fixups (string references: LEA [rip+str_label])
// Apply RIP fixups (string references and BSS globals: LEA [rip+label])
Integer ripN = this.enc.ripFixOffs.length();
Integer bufLen = this.enc.buf.length();
Integer ri = 0;
while (ri < ripN) {
Integer ripOff = this.enc.ripFixOffs.get(ri) as Integer;
String ripLbl = this.enc.ripFixLbls.get(ri) as String;
ri = ri + 1;
Integer strOff = -1;
Integer tgtOff = -1;
// Search string labels
Integer sj = 0;
while (sj < S) {
String nm = this.strNames.get(sj) as String;
if (nm == ripLbl) { strOff = strOffsets.get(sj) as Integer; }
if (nm == ripLbl) { tgtOff = strOffsets.get(sj) as Integer; }
sj = sj + 1;
}
if (strOff < 0) {
// Search BSS labels
if (tgtOff < 0) {
Integer bk = 0;
while (bk < this.bssLabels.length()) {
String bnm = this.bssLabels.get(bk) as String;
if (bnm == ripLbl) { tgtOff = bssOffsets.get(bk) as Integer; }
bk = bk + 1;
}
}
if (tgtOff < 0) {
IO.println("arc: [warn] ELF: missing label ${ripLbl}");
} else if (ripOff + 3 >= bufLen) {
IO.println("arc: [warn] ELF: bounds ripOff=${ripOff} bufLen=${bufLen}");
} else {
Integer targetVA = dataVA + strOff;
Integer targetVA = dataVA + tgtOff;
Integer ripRVA = textVA + ripOff + 4;
Integer rel32 = targetVA - ripRVA;
List<Integer> buf = this.enc.buf;
Expand Down Expand Up @@ -197,8 +231,8 @@ public class ELFWriter {
this.w64(file, dataFileOff); // p_offset
this.w64(file, dataVA); // p_vaddr
this.w64(file, dataVA); // p_paddr
this.w64(file, dataSize); // p_filesz
this.w64(file, dataSize); // p_memsz
this.w64(file, dataFileSize); // p_filesz (string data, no BSS in file)
this.w64(file, dataMemSize); // p_memsz (includes BSS — kernel zero-fills)
this.w64(file, pageSize); // p_align

// Pad headers to textFileOff (0x1000)
Expand All @@ -220,7 +254,11 @@ public class ELFWriter {
this.wStr(file, cs);
sq = sq + 1;
}
if (S == 0) { this.wPad(file, 8); }
// Pad to alignment boundary (8-byte for BSS globals)
if (dataAligned > strCursor) {
this.wPad(file, dataAligned - strCursor);
}
if (dataFileSize <= 8 && S == 0) { this.wPad(file, 8); }

return file;
}
Expand Down
Loading
Loading