Skip to content
Closed
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
14 changes: 14 additions & 0 deletions neomacs-build-test/macos/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,25 @@ Use a single script to launch Docker-OSX and run the full Neomacs build:

Defaults:

- Docker image: `neomacs/docker-osx:naked-auto-local`
- Docker container: `neomacs-macos15-test`
- CPU: `24` cores (`SMP=24`, `CORES=24`)
- SSH: `127.0.0.1:50922`, user `user`, password `alpine`
- Build mode: `full`
- Cleanup: container is stopped/removed automatically when script exits (`CLEANUP_CONTAINER=1`)
- Disk mode: persistent mounted disk (`USE_PERSISTENT_DISK=1`)

Fresh-from-base mode (recommended for no-manual, always-fresh runs):

```bash
FRESH_OVERLAY_DISK=1 \
BASE_DISK_IMAGE=/home/exec/virtual/macos15/mac_hdd_ng_sequoia.img \
./docker-osx-full-build.sh full
```

This creates a temporary qcow2 overlay on each run and deletes it on exit.

Note: `naked`/`naked-auto` without an installed disk usually boots Recovery.

Rust-only mode:

Expand Down
84 changes: 61 additions & 23 deletions neomacs-build-test/macos/docker-osx-full-build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONTAINER_NAME="${CONTAINER_NAME:-neomacs-macos15-test}"
DOCKER_IMAGE="${DOCKER_IMAGE:-neomacs/docker-osx:naked-auto-local}"
MACOS_DISK_IMAGE="${MACOS_DISK_IMAGE:-/home/exec/virtual/macos15/mac_hdd_ng_sequoia.img}"
USE_PERSISTENT_DISK="${USE_PERSISTENT_DISK:-1}"
FRESH_OVERLAY_DISK="${FRESH_OVERLAY_DISK:-0}"
BASE_DISK_IMAGE="${BASE_DISK_IMAGE:-$MACOS_DISK_IMAGE}"

HOST_SSH_PORT="${HOST_SSH_PORT:-50922}"
MACOS_USERNAME="${MACOS_USERNAME:-user}"
Expand All @@ -25,11 +28,14 @@ DOCKER_CPU="${DOCKER_CPU:-Haswell-noTSX}"
DOCKER_CPUID_FLAGS="${DOCKER_CPUID_FLAGS:-kvm=on,vendor=GenuineIntel,+invtsc,vmware-cpuid-freq=on}"
OSX_COMMANDS="${OSX_COMMANDS:-while true; do sleep 3600; done}"

RECREATE_CONTAINER="${RECREATE_CONTAINER:-0}"
RECREATE_CONTAINER="${RECREATE_CONTAINER:-1}"
CLEANUP_CONTAINER="${CLEANUP_CONTAINER:-1}"
SSH_LOGIN_TIMEOUT_SEC="${SSH_LOGIN_TIMEOUT_SEC:-900}"
SSH_POLL_INTERVAL_SEC="${SSH_POLL_INTERVAL_SEC:-5}"

MOUNT_DISK_IMAGE=""
RUN_DISK_IMAGE=""

MODE="${1:-full}"
case "$MODE" in
full|rust-only) ;;
Expand All @@ -51,9 +57,31 @@ require_cmd sshpass
require_cmd ssh
require_cmd rg

if [[ ! -f "$MACOS_DISK_IMAGE" ]]; then
echo "macOS disk image not found: $MACOS_DISK_IMAGE" >&2
exit 1
if [[ "$FRESH_OVERLAY_DISK" == "1" ]]; then
require_cmd qemu-img
if [[ ! -f "$BASE_DISK_IMAGE" ]]; then
echo "base macOS disk image not found: $BASE_DISK_IMAGE" >&2
exit 1
fi
elif [[ "$USE_PERSISTENT_DISK" == "1" ]]; then
if [[ ! -f "$MACOS_DISK_IMAGE" ]]; then
echo "macOS disk image not found: $MACOS_DISK_IMAGE" >&2
exit 1
fi
fi

if [[ "$FRESH_OVERLAY_DISK" == "1" ]]; then
MOUNT_DISK_IMAGE="/tmp/${CONTAINER_NAME}-run-$(date +%Y%m%d-%H%M%S)-$$.qcow2"
RUN_DISK_IMAGE="$MOUNT_DISK_IMAGE"
echo "Creating fresh overlay disk from base: $BASE_DISK_IMAGE"
qemu-img create -f qcow2 -F qcow2 -b "$BASE_DISK_IMAGE" "$MOUNT_DISK_IMAGE" >/dev/null
elif [[ "$USE_PERSISTENT_DISK" == "1" ]]; then
MOUNT_DISK_IMAGE="$MACOS_DISK_IMAGE"
fi

if [[ -z "$MOUNT_DISK_IMAGE" ]] && [[ "$DOCKER_IMAGE" == *"naked"* ]]; then
echo "Warning: DOCKER_IMAGE=$DOCKER_IMAGE is a naked image; without a persistent installed disk it usually boots Recovery." >&2
echo "Use FRESH_OVERLAY_DISK=1 with BASE_DISK_IMAGE=<installed macOS qcow2> to get fresh runs without Recovery." >&2
fi

container_exists() {
Expand All @@ -72,6 +100,10 @@ cleanup_container() {
echo "Cleaning up container: $CONTAINER_NAME"
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
fi
if [[ -n "$RUN_DISK_IMAGE" && -f "$RUN_DISK_IMAGE" ]]; then
echo "Removing overlay disk: $RUN_DISK_IMAGE"
rm -f "$RUN_DISK_IMAGE" || true
fi
}

trap cleanup_container EXIT INT TERM
Expand All @@ -92,25 +124,31 @@ launch_container() {
fi

echo "Launching Docker-OSX container: $CONTAINER_NAME"
docker run -d \
--name "$CONTAINER_NAME" \
--device /dev/kvm \
-p "${HOST_SSH_PORT}:10022" \
-v "${MACOS_DISK_IMAGE}:/image" \
-e SHORTNAME="${DOCKER_SHORTNAME}" \
-e GENERATE_UNIQUE="${GENERATE_UNIQUE}" \
-e NOPICKER="${NOPICKER}" \
-e TERMS_OF_USE=i_agree \
-e USERNAME="${MACOS_USERNAME}" \
-e PASSWORD="${MACOS_PASSWORD}" \
-e RAM="${DOCKER_RAM_GB}" \
-e SMP="${DOCKER_CORES}" \
-e CORES="${DOCKER_CORES}" \
-e CPU="${DOCKER_CPU}" \
-e CPUID_FLAGS="${DOCKER_CPUID_FLAGS}" \
-e AUDIO_DRIVER=none \
-e OSX_COMMANDS="${OSX_COMMANDS}" \
"$DOCKER_IMAGE" >/dev/null
run_args=(
-d
--name "$CONTAINER_NAME"
--device /dev/kvm
-p "${HOST_SSH_PORT}:10022"
-e SHORTNAME="${DOCKER_SHORTNAME}"
-e GENERATE_UNIQUE="${GENERATE_UNIQUE}"
-e NOPICKER="${NOPICKER}"
-e TERMS_OF_USE=i_agree
-e USERNAME="${MACOS_USERNAME}"
-e PASSWORD="${MACOS_PASSWORD}"
-e RAM="${DOCKER_RAM_GB}"
-e SMP="${DOCKER_CORES}"
-e CORES="${DOCKER_CORES}"
-e CPU="${DOCKER_CPU}"
-e CPUID_FLAGS="${DOCKER_CPUID_FLAGS}"
-e AUDIO_DRIVER=none
-e OSX_COMMANDS="${OSX_COMMANDS}"
)
if [[ -n "$MOUNT_DISK_IMAGE" ]]; then
run_args+=(-v "${MOUNT_DISK_IMAGE}:/image")
fi
run_args+=("$DOCKER_IMAGE")

docker run "${run_args[@]}" >/dev/null
}

verify_qemu_cpu_config() {
Expand Down
68 changes: 68 additions & 0 deletions test/neomacs/cjk-cursor-repro.el
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
;;; cjk-cursor-repro.el --- Repro fixture for CJK cursor alignment -*- lexical-binding: t -*-

;; This fixture prepares a buffer with mixed ASCII/CJK content and places point
;; in known positions so renderer cursor placement can be visually inspected.

(defvar cjk-cursor-repro-font-candidates
'("Sarasa Mono SC"
"Noto Sans Mono CJK SC"
"Noto Sans CJK SC"
"PingFang SC"
"Hiragino Sans GB"
"STHeiti"
"WenQuanYi Zen Hei")
"Font candidates to improve CJK repro consistency across platforms.")

(defun cjk-cursor-repro--first-available-font ()
"Return first available font from `cjk-cursor-repro-font-candidates'."
(catch 'found
(dolist (name cjk-cursor-repro-font-candidates)
(when (find-font (font-spec :name name))
(throw 'found name)))
nil))

(defun cjk-cursor-repro--setup-font ()
"Apply a CJK-capable font when available."
(let ((font (cjk-cursor-repro--first-available-font)))
(when font
(set-frame-font (format "%s-22" font) t t)
(message "CJK repro font: %s" font))))

(defun cjk-cursor-repro--insert-content ()
"Insert mixed-width text designed to expose cursor/glyph misalignment."
(insert "CJK Cursor Repro\n")
(insert "Use arrow keys to move point across mixed-width text.\n\n")
(insert "RULER: 1234567890123456789012345678901234567890\n")
(insert "ASCII: ........................................\n")
(insert "CJK : 你好世界你好世界你好世界你好世界\n")
(insert "MIX1 : A汉B字C测D试E中F文G混H排I\n")
(insert "MIX2 : 123汉字abcかなカナ한글XYZ\n")
(insert "FULL : ABC123,。、;:?!\n")
(insert "MIX3 : []{}()<>|!@# 汉字 / kana かな / hangul 한글\n")
(insert "\nTarget line for screenshot:\n")
(insert "TARGET: A汉B字C测D试E中F文G混H排I\n"))

(defun cjk-cursor-repro--goto-target ()
"Move point to a deterministic position on the TARGET line."
(goto-char (point-min))
(search-forward "TARGET: ")
;; Move onto the first CJK char on target line ("汉").
(forward-char 1)
(message "Point prepared on TARGET CJK char for screenshot"))

(defun cjk-cursor-repro-start ()
"Create and display the CJK cursor repro buffer."
(switch-to-buffer (get-buffer-create "*CJK Cursor Repro*"))
(setq-local cursor-type 'box)
(setq-local truncate-lines t)
(erase-buffer)
(cjk-cursor-repro--setup-font)
(cjk-cursor-repro--insert-content)
(goto-char (point-min))
(cjk-cursor-repro--goto-target)
(blink-cursor-mode -1)
(message "CJK cursor repro ready"))

(cjk-cursor-repro-start)

;;; cjk-cursor-repro.el ends here
129 changes: 129 additions & 0 deletions test/neomacs/repro-cjk-cursor.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
#!/usr/bin/env bash
# Reproduce CJK cursor alignment behavior and save a screenshot.
# Usage: ./test/neomacs/repro-cjk-cursor.sh

set -euo pipefail

cd "$(dirname "$0")/../.."

LOG="/tmp/cjk-cursor-repro.log"
SCREENSHOT_CJK="/tmp/cjk-cursor-repro-cjk.png"
SCREENSHOT_MOVED="/tmp/cjk-cursor-repro-moved.png"
XVFB_PID=""
EMACS_PID=""

setup_lib_path() {
local xcursor_so=""
local xkb_so=""
xcursor_so=$(find /nix/store -maxdepth 3 -name 'libXcursor.so.1' 2>/dev/null | head -1 || true)
xkb_so=$(find /nix/store -maxdepth 3 -name 'libxkbcommon-x11.so' 2>/dev/null | head -1 || true)

local extra_path=""
if [ -n "$xcursor_so" ]; then
extra_path="$(dirname "$xcursor_so")"
fi
if [ -n "$xkb_so" ]; then
extra_path="${extra_path:+$extra_path:}$(dirname "$xkb_so")"
fi

if [ -n "$extra_path" ]; then
export LD_LIBRARY_PATH="${extra_path}${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
fi
}

setup_display() {
local current_display="${DISPLAY:-}"
if [ -z "$current_display" ] || ! DISPLAY="$current_display" xdotool getactivewindow >/dev/null 2>&1; then
rm -f /tmp/.X99-lock
kill $(pgrep -f "Xvfb :99") 2>/dev/null || true
sleep 1
Xvfb :99 -screen 0 1920x1080x24 -ac >/tmp/cjk-cursor-repro-xvfb.log 2>&1 &
XVFB_PID=$!
sleep 2
if ! kill -0 "$XVFB_PID" 2>/dev/null; then
echo "ERROR: failed to start Xvfb on :99"
exit 1
fi
export DISPLAY=:99
else
export DISPLAY="$current_display"
fi
}

cleanup() {
if [ -n "$EMACS_PID" ]; then
kill "$EMACS_PID" 2>/dev/null || true
wait "$EMACS_PID" 2>/dev/null || true
fi
if [ -n "$XVFB_PID" ]; then
kill "$XVFB_PID" 2>/dev/null || true
wait "$XVFB_PID" 2>/dev/null || true
fi
}
trap cleanup EXIT

echo "=== CJK Cursor Repro ==="

if [ ! -x ./src/emacs ]; then
echo "ERROR: ./src/emacs not found. Build neomacs first."
exit 1
fi

for bin in xdotool import Xvfb; do
if ! command -v "$bin" >/dev/null 2>&1; then
echo "ERROR: missing dependency: $bin"
exit 1
fi
done

rm -f "$LOG" "$SCREENSHOT_CJK" "$SCREENSHOT_MOVED"
setup_lib_path
setup_display

RUST_LOG=neomacs_display=debug ./src/emacs -Q \
-l test/neomacs/cjk-cursor-repro.el 2>"$LOG" &
EMACS_PID=$!

sleep 6

if ! kill -0 "$EMACS_PID" 2>/dev/null; then
echo "ERROR: emacs exited early"
tail -n 80 "$LOG" || true
exit 1
fi

WIN_ID=$(xdotool search --name "CJK Cursor Repro|emacs" 2>/dev/null | head -1 || true)
if [ -z "${WIN_ID:-}" ]; then
echo "ERROR: failed to find Emacs window"
tail -n 80 "$LOG" || true
exit 1
fi

xdotool windowactivate --sync "$WIN_ID" 2>/dev/null || true
xdotool windowfocus --sync "$WIN_ID" 2>/dev/null || true
sleep 1

import -window "$WIN_ID" "$SCREENSHOT_CJK" 2>/dev/null || true

# Drive cursor through mixed-width content before capture.
xdotool key --window "$WIN_ID" Home
sleep 0.1
for _ in $(seq 1 14); do
xdotool key --window "$WIN_ID" Right
sleep 0.06
done
sleep 1

import -window "$WIN_ID" "$SCREENSHOT_MOVED" 2>/dev/null || true

if [ ! -f "$SCREENSHOT_CJK" ]; then
echo "ERROR: initial screenshot capture failed"
tail -n 80 "$LOG" || true
exit 1
fi

echo "Screenshot (cursor-on-CJK): $SCREENSHOT_CJK"
if [ -f "$SCREENSHOT_MOVED" ]; then
echo "Screenshot (cursor-moved): $SCREENSHOT_MOVED"
fi
echo "Log: $LOG"