Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
7 changes: 7 additions & 0 deletions .github/scripts/check-native-header.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#include "tui_test.h"

int main(void) {
TuiWaitOptions options = {0};
options.not_ = true;
return !options.not_;
}
62 changes: 37 additions & 25 deletions .github/scripts/release/package-node.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,48 +6,60 @@ const bindingsDirectory = path.resolve("bindings/js");
const nativePackagesDirectory = path.join(bindingsDirectory, "npm");
const outputDirectory = path.resolve("package-artifacts/npm");
const packagePath = path.join(bindingsDirectory, "package.json");
const originalPackage = fs.readFileSync(packagePath, "utf8");
const rootPackage = JSON.parse(originalPackage);
rootPackage.optionalDependencies = {};

const runNpm = (args) => execFileSync("npm", args, { stdio: "inherit" });

const nativePackages = fs
.readdirSync(nativePackagesDirectory, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => path.join(nativePackagesDirectory, entry.name))
.sort();
const targets = Object.values(rootPackage.nativeTargets).sort();

if (nativePackages.length === 0) {
throw new Error(`No native packages found in ${nativePackagesDirectory}`);
if (targets.length === 0) {
throw new Error(`No native targets configured in ${packagePath}`);
}

fs.mkdirSync(outputDirectory, { recursive: true });

for (const nativePackage of nativePackages) {
const hasNativeAddon = fs
.readdirSync(nativePackage, { withFileTypes: true })
.some((entry) => entry.isFile() && entry.name.endsWith(".node"));
if (!hasNativeAddon) {
throw new Error(`Missing native addon in ${nativePackage}`);
for (const tag of targets) {
const [platform, arch, abi] = tag.split("-");
const extension = { darwin: "dylib", linux: "so", win32: "dll" }[platform];
if (!extension) throw new Error(`Unsupported native platform: ${platform}`);
const filename = `tui-test.${tag}.${extension}`;
const nativePackage = path.join(nativePackagesDirectory, tag);
const manifestPath = path.join(nativePackage, "package.json");
const manifest = {
name: `${rootPackage.name}-${tag}`,
version: rootPackage.version,
description: rootPackage.description,
main: filename,
files: [filename],
os: [platform],
cpu: [arch],
...(platform === "linux" ? { libc: [abi === "gnu" ? "glibc" : abi] } : {}),
license: rootPackage.license,
author: rootPackage.author,
repository: rootPackage.repository,
engines: rootPackage.engines,
publishConfig: rootPackage.publishConfig,
};
const artifact = path.join(bindingsDirectory, "artifacts", filename);
if (!fs.existsSync(artifact)) {
throw new Error(`Missing shared library for ${manifest.name}: ${artifact}`);
}

fs.mkdirSync(nativePackage, { recursive: true });
fs.copyFileSync(artifact, path.join(nativePackage, filename));
rootPackage.optionalDependencies[manifest.name] = rootPackage.version;
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
fs.writeFileSync(path.join(nativePackage, "README.md"), `# ${manifest.name}\n\nThis package contains the tui-test shared library for ${tag}.\n`);
runNpm([
"pack",
nativePackage,
"--ignore-scripts",
"--pack-destination",
outputDirectory,
]);
}

const originalPackage = fs.readFileSync(packagePath, "utf8");
const rootPackage = JSON.parse(originalPackage);
rootPackage.optionalDependencies = {};

for (const nativePackage of nativePackages) {
const manifest = JSON.parse(
fs.readFileSync(path.join(nativePackage, "package.json"), "utf8"),
);
rootPackage.optionalDependencies[manifest.name] = manifest.version;
}

try {
fs.writeFileSync(
packagePath,
Expand Down
64 changes: 64 additions & 0 deletions .github/scripts/release/package-node.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import test from "node:test";

const script = fileURLToPath(new URL("./package-node.mjs", import.meta.url));
const { nativeTargets } = JSON.parse(fs.readFileSync(new URL("../../../bindings/js/package.json", import.meta.url), "utf8"));
const targets = Object.values(nativeTargets);
const library = (platform) => `tui-test.${platform}.${platform.startsWith("darwin") ? "dylib" : platform.startsWith("win32") ? "dll" : "so"}`;

function fixture(t) {
const target = path.resolve("target");
fs.mkdirSync(target, { recursive: true });
const directory = fs.mkdtempSync(path.join(target, "package-node-test-"));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const bindings = path.join(directory, "bindings/js");
fs.mkdirSync(path.join(bindings, "artifacts"), { recursive: true });
fs.writeFileSync(path.join(bindings, "package.json"), JSON.stringify({
name: "@fixture/tui-test",
version: "1.0.0",
files: ["index.js"],
dependencies: { koffi: "^3.3.3" },
nativeTargets,
}));
fs.writeFileSync(path.join(bindings, "index.js"), "export {};\n");
for (const platform of targets) {
fs.writeFileSync(path.join(bindings, "artifacts", library(platform)), "shared library fixture");
}
return directory;
}

test("platform packages include the shared library at the release version", (t) => {
const directory = fixture(t);
const packagePath = path.join(directory, "bindings/js/package.json");
const original = fs.readFileSync(packagePath, "utf8");
execFileSync(process.execPath, [script], { cwd: directory, stdio: "pipe" });
assert.equal(fs.readFileSync(packagePath, "utf8"), original);
for (const platform of targets) {
const archive = path.join(directory, "package-artifacts/npm", `fixture-tui-test-${platform}-1.0.0.tgz`);
const entries = execFileSync("tar", ["-tzf", archive], { encoding: "utf8" }).split("\n");
assert.ok(entries.includes(`package/${library(platform)}`));
assert.ok(!entries.some((entry) => entry.endsWith(".node")));
const manifest = JSON.parse(execFileSync("tar", ["-xOf", archive, "package/package.json"], { encoding: "utf8" }));
assert.equal(manifest.version, "1.0.0");
assert.deepEqual(manifest.os, [platform.split("-")[0]]);
assert.deepEqual(manifest.cpu, [platform.split("-")[1]]);
if (platform.startsWith("linux")) assert.deepEqual(manifest.libc, [platform.endsWith("gnu") ? "glibc" : "musl"]);
}
const archive = path.join(directory, "package-artifacts/npm/fixture-tui-test-1.0.0.tgz");
const manifest = JSON.parse(execFileSync("tar", ["-xOf", archive, "package/package.json"], { encoding: "utf8" }));
assert.equal(manifest.dependencies.koffi, "^3.3.3");
assert.deepEqual(manifest.optionalDependencies, Object.fromEntries(targets.map((platform) => [`@fixture/tui-test-${platform}`, "1.0.0"])));
});

test("packaging rejects a missing shared library artifact", (t) => {
const directory = fixture(t);
fs.unlinkSync(path.join(directory, "bindings/js/artifacts", library("darwin-arm64")));
assert.throws(
() => execFileSync(process.execPath, [script], { cwd: directory, stdio: "pipe" }),
/Missing shared library/,
);
});
30 changes: 30 additions & 0 deletions .github/scripts/release/prepare-python-linux.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
set -euo pipefail

curl --proto '=https' --tlsv1.2 -fsSL https://sh.rustup.rs |
sh -s -- -y --profile minimal --default-toolchain stable --no-modify-path

version=0.16.0
case "$(uname -m)" in
x86_64|amd64)
arch=x86_64
sha256=70e49664a74374b48b51e6f3fdfbf437f6395d42509050588bd49abe52ba3d00
;;
aarch64|arm64)
arch=aarch64
sha256=ea4b09bfb22ec6f6c6ceac57ab63efb6b46e17ab08d21f69f3a48b38e1534f17
;;
*)
echo "unsupported Zig host architecture: $(uname -m)" >&2
exit 1
;;
esac

mkdir -p "$HOME/.local/bin" "$HOME/.local/lib" "$HOME/.cache/tui-test-release"
archive="$HOME/.cache/tui-test-release/zig-$arch-linux-$version.tar.xz"
curl --proto '=https' --tlsv1.2 -fsSLo "$archive" \
"https://ziglang.org/download/$version/zig-$arch-linux-$version.tar.xz"
echo "$sha256 $archive" | sha256sum -c -
tar -xJf "$archive" -C "$HOME/.local/lib"
ln -sf "$HOME/.local/lib/zig-$arch-linux-$version/zig" "$HOME/.local/bin/zig"
"$HOME/.local/bin/zig" version
58 changes: 52 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,21 +53,67 @@ jobs:
cache: npm
cache-dependency-path: bindings/js/package-lock.json

- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
with:
go-version-file: bindings/go/go.mod
cache-dependency-path: bindings/go/go.sum

- run: cargo fmt --all -- --check
- name: Verify generated native declarations
run: cargo run --locked -p tui-test-bindgen -- --check
- name: Compile the native header as C and C++
if: runner.os != 'Windows'
run: |
cc -std=c11 -fsyntax-only -Icrates/tui-test-ffi/include .github/scripts/check-native-header.c
c++ -std=c++17 -x c++ -fsyntax-only -Icrates/tui-test-ffi/include .github/scripts/check-native-header.c
- name: Test native package assembly
if: runner.os == 'Linux'
run: node --test .github/scripts/release/package-node.test.mjs
- run: cargo clippy --workspace --all-targets --all-features -- -D warnings
- run: cargo clippy -p tui-test-rs --all-targets --no-default-features -- -D warnings
- run: cargo build
- run: cargo build -p tui-test-cli -p tui-test-ffi
- run: cargo test --workspace -- --test-threads=1

- name: Test Go binding
shell: bash
run: |
case "$RUNNER_OS" in
Linux) library=libtui_test_ffi.so ;;
macOS) library=libtui_test_ffi.dylib ;;
Windows) library=tui_test_ffi.dll ;;
*) echo "Unsupported runner: $RUNNER_OS" >&2; exit 1 ;;
esac
export TUI_TEST_NATIVE_LIBRARY="$GITHUB_WORKSPACE/target/debug/$library"
export CGO_ENABLED=0
cd bindings/go
go test ./...
go run ./examples/smoke

- uses: taiki-e/install-action@83ac0ad63c0167e6f06796fab0fce28db1bf3db0 # v2.87.22
if: runner.os == 'Linux'
with:
tool: cargo-zigbuild

- name: Test Go binding on musl
if: runner.os == 'Linux'
run: |
rustup target add x86_64-unknown-linux-musl
RUSTFLAGS="${RUSTFLAGS:-} -C target-feature=-crt-static" cargo zigbuild --locked -p tui-test-ffi --target x86_64-unknown-linux-musl
test -f target/x86_64-unknown-linux-musl/debug/libtui_test_ffi.so
cd bindings/go
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags '-I /lib/ld-musl-x86_64.so.1' -o ../../target/go-musl-smoke ./examples/smoke
cd ../..
docker run --rm --platform linux/amd64 \
-v "$PWD/target:/work:ro" \
-e TUI_TEST_NATIVE_LIBRARY=/work/x86_64-unknown-linux-musl/debug/libtui_test_ffi.so \
alpine:3.23 sh -c 'apk add --no-cache libstdc++ && /work/go-musl-smoke'

- name: Install Python binding
run: python -m pip install --disable-pip-version-check -e ./bindings/python

- name: Test Python binding
run: python -m unittest discover -s bindings/python/tests -v

- name: Verify generated Python stubs
run: python bindings/python/scripts/generate_stubs.py --check

- name: Install Node dependencies
run: npm ci --prefix bindings/js

Expand All @@ -89,10 +135,10 @@ jobs:
- name: Test Node binding
run: npm run test:node --prefix bindings/js

- name: Verify generated Node loader
- name: Verify generated Node declarations
shell: bash
run: |
git diff --exit-code -- bindings/js/native/index.js bindings/js/native/index.d.ts
cargo run --locked -p tui-test-bindgen -- --check
! grep -Eq 'JsonValue|Promise<unknown>|\brequest\(|\bBuffer\b' bindings/js/native/index.d.ts

- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
Expand Down
Loading
Loading