From e893c45d4f182fc2a880c2d12e62c3019d9c3249 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 1 Oct 2026 21:27:22 +0200 Subject: [PATCH] Add Windows cross-build and Gitea release; share CI setup - just windows-deps / windows / package-windows: cross-compile with Odin objects + lld-link against an xwin SDK, static VC runtime - Release workflow runs on v* tags, builds Linux and Windows packages, uploads them to the package registry and creates a Gitea release - Shared toolchain setup as a local composite action Co-Authored-By: Claude Opus 5.5 --- .gitea/actions/setup/action.yaml | 44 +++++++++++++++++++ .gitea/workflows/gitea-build.yaml | 26 ++--------- .gitea/workflows/gitea-release.yaml | 68 ++++++++++++++++------------- AGENTS.md | 9 ++-- README.md | 16 +++++-- justfile | 68 ++++++++++++++++++++++++++--- 6 files changed, 165 insertions(+), 66 deletions(-) create mode 100644 .gitea/actions/setup/action.yaml diff --git a/.gitea/actions/setup/action.yaml b/.gitea/actions/setup/action.yaml new file mode 100644 index 0000000..acef5e3 --- /dev/null +++ b/.gitea/actions/setup/action.yaml @@ -0,0 +1,44 @@ +# +# .gitea/actions/setup/action.yaml +# +# Installs the toolchain shared by all workflows: system packages, Odin and just. +# + +name: "Setup Odin toolchain" +description: "Install system packages, Odin and just" + +inputs: + odin-version: + description: "Odin release tag (keep in sync with odin_version in the justfile)" + default: "dev-2026-09" + packages: + description: "Extra apt packages to install" + default: "" + +runs: + using: "composite" + steps: + - name: Install System Packages + shell: bash + run: | + SUDO=""; command -v sudo >/dev/null && SUDO=sudo + $SUDO apt-get update + # clang links Odin programs on Linux; raylib needs X11 + $SUDO apt-get install -y --no-install-recommends \ + clang libx11-dev curl ca-certificates unzip ${{ inputs.packages }} + + - name: Install Odin + shell: bash + run: | + curl -fsSL -o /tmp/odin.tar.gz \ + https://github.com/odin-lang/Odin/releases/download/${{ inputs.odin-version }}/odin-linux-amd64-${{ inputs.odin-version }}.tar.gz + mkdir -p "$HOME/odin" + tar -xzf /tmp/odin.tar.gz -C "$HOME/odin" --strip-components=1 + echo "$HOME/odin" >> "$GITHUB_PATH" + + - name: Install just + shell: bash + run: | + mkdir -p "$HOME/.local/bin" + curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to "$HOME/.local/bin" + echo "$HOME/.local/bin" >> "$GITHUB_PATH" diff --git a/.gitea/workflows/gitea-build.yaml b/.gitea/workflows/gitea-build.yaml index d17d05a..65b4315 100644 --- a/.gitea/workflows/gitea-build.yaml +++ b/.gitea/workflows/gitea-build.yaml @@ -1,7 +1,7 @@ # # .gitea/workflows/gitea-build.yaml # -# Builds and tests on every push and pull request to main and develop. +# Checks, tests and builds on every push and pull request to main and develop. # name: "Build and Test" @@ -11,9 +11,6 @@ on: pull_request: branches: [main, develop] -env: - ODIN_VERSION: dev-2026-09 - jobs: build: runs-on: ubuntu-latest @@ -21,25 +18,8 @@ jobs: - name: Checkout Repository uses: actions/checkout@v4 - - name: Install System Dependencies - run: | - SUDO=""; command -v sudo >/dev/null && SUDO=sudo - $SUDO apt-get update - # clang links Odin programs; raylib needs X11 on Linux - $SUDO apt-get install -y --no-install-recommends clang libx11-dev curl ca-certificates - - - name: Install Odin - run: | - curl -fsSL -o odin.tar.gz https://github.com/odin-lang/Odin/releases/download/$ODIN_VERSION/odin-linux-amd64-$ODIN_VERSION.tar.gz - mkdir -p "$HOME/odin" - tar -xzf odin.tar.gz -C "$HOME/odin" --strip-components=1 - echo "$HOME/odin" >> "$GITHUB_PATH" - - - name: Install just - run: | - mkdir -p "$HOME/.local/bin" - curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to "$HOME/.local/bin" - echo "$HOME/.local/bin" >> "$GITHUB_PATH" + - name: Setup Toolchain + uses: ./.gitea/actions/setup - name: Check run: | diff --git a/.gitea/workflows/gitea-release.yaml b/.gitea/workflows/gitea-release.yaml index 8131e27..285f61d 100644 --- a/.gitea/workflows/gitea-release.yaml +++ b/.gitea/workflows/gitea-release.yaml @@ -1,47 +1,33 @@ # # .gitea/workflows/gitea-release.yaml # -# Builds a release package on version branches (e.g. 1.0) and uploads it to -# the Gitea generic package registry. +# On a version tag (e.g. v1.0): tests, builds Linux and Windows packages, +# uploads them to the Gitea generic package registry and creates a Gitea release. # -name: "Publish" +name: "Release" on: push: - branches: - - '*.*' + tags: + - 'v*' env: - ODIN_VERSION: dev-2026-09 PROJECT_NAME: HanjaKanaMun - GAME_VERSION: ${{gitea.ref_name}} jobs: - build: + release: runs-on: ubuntu-latest steps: - name: Checkout Repository uses: actions/checkout@v4 - - name: Install System Dependencies - run: | - SUDO=""; command -v sudo >/dev/null && SUDO=sudo - $SUDO apt-get update - # clang links Odin programs; raylib needs X11 on Linux - $SUDO apt-get install -y --no-install-recommends clang libx11-dev curl ca-certificates + - name: Setup Toolchain + uses: ./.gitea/actions/setup + with: + packages: lld zip - - name: Install Odin - run: | - curl -fsSL -o odin.tar.gz https://github.com/odin-lang/Odin/releases/download/$ODIN_VERSION/odin-linux-amd64-$ODIN_VERSION.tar.gz - mkdir -p "$HOME/odin" - tar -xzf odin.tar.gz -C "$HOME/odin" --strip-components=1 - echo "$HOME/odin" >> "$GITHUB_PATH" - - - name: Install just - run: | - mkdir -p "$HOME/.local/bin" - curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to "$HOME/.local/bin" - echo "$HOME/.local/bin" >> "$GITHUB_PATH" + - name: Set Version + run: echo "GAME_VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV" - name: Test run: just test @@ -49,10 +35,30 @@ jobs: - name: Linux Build run: just package "$GAME_VERSION" - - name: Upload Artifact Linux + - name: Windows Build + run: | + # Debian/Ubuntu may only ship a versioned lld-link + LLD_LINK="$(command -v lld-link || ls /usr/lib/llvm-*/bin/lld-link | sort -V | tail -1)" + export LLD_LINK + just windows-deps + just package-windows "$GAME_VERSION" + + - name: Upload Artifacts run: | ls -la build - FILE="${PROJECT_NAME}_${GAME_VERSION}_linux_x86_64.tar.gz" - curl --fail --user ${{ github.repository_owner }}:${{ secrets.TOKEN }} \ - --upload-file "build/$FILE" \ - https://gitea.sanplex.xyz/api/packages/sansan/generic/$PROJECT_NAME/$GAME_VERSION/$FILE + for FILE in build/${PROJECT_NAME}_${GAME_VERSION}_*; do + NAME="$(basename "$FILE")" + echo "Uploading $NAME" + curl --fail --user ${{ github.repository_owner }}:${{ secrets.TOKEN }} \ + --upload-file "$FILE" \ + https://gitea.sanplex.xyz/api/packages/sansan/generic/$PROJECT_NAME/$GAME_VERSION/$NAME + done + + - name: Create Release + uses: akkuman/gitea-release-action@v1 + env: + NODE_OPTIONS: '--experimental-fetch' # if nodejs < 18 + with: + files: |- + build/${{ env.PROJECT_NAME }}_*.tar.gz + build/${{ env.PROJECT_NAME }}_*.zip diff --git a/AGENTS.md b/AGENTS.md index c4cf186..2117e4d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -33,6 +33,7 @@ Commands (see `justfile`): `just run`, `just check`, `just test`, `just package 2. **Old font failure root cause**: `DroidSansJapanese.ttf` was a GitHub HTML page saved from a blob URL, not a font. Always download fonts from raw URLs and check them with `file`. 3. **Thai shaping**: raylib does no text shaping, so Thai combining vowels/tone marks may be misplaced. Prefer standalone consonants on Thai cards. 4. Tests share the memory game's global state, so they must run single-threaded (`just test` sets `ODIN_TEST_THREADS=1`). +6. **Windows builds are cross-compiled**: Odin can't link Windows binaries on Linux, so `just windows` builds objects with `-build-mode:obj` and links with `lld-link` against an xwin SDK. The VC runtime is linked statically; only Windows system DLLs are needed. Not yet tested on a real Windows machine. 5. The game changes directory to the executable's folder at startup so `assets/` is found from anywhere. ## File Structure @@ -43,9 +44,11 @@ HanjaKanaMun/ ├── justfile # build/run/check/test/package recipes ├── ols.json # ols language server settings (neovim) ├── odinfmt.json # odinfmt formatter settings (4 spaces, LF) -├── .gitea/workflows/ -│ ├── gitea-build.yaml # check + test + build on main/develop -│ └── gitea-release.yaml # package on version branches (*.*), upload to Gitea packages +├── .gitea/ +│ ├── actions/setup/ # shared CI toolchain setup (apt, Odin, just) +│ └── workflows/ +│ ├── gitea-build.yaml # check + test + build on main/develop +│ └── gitea-release.yaml # on tags v*: Linux + Windows packages, package registry upload, Gitea release ├── main.odin # Window/audio setup, screen state machine ├── src/ │ ├── themes/themes.odin # Theme struct, flag colors, Screen/Flag enums, layout constants diff --git a/README.md b/README.md index bccfba4..49ef728 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,15 @@ Requires [Odin](https://odin-lang.org) (dev-2026-09) and [just](https://just.sys just run # build and play (debug) just check # type-check with -vet just test # run tests -just package 1.0 # release build -> build/HanjaKanaMun_1.0__.tar.gz +just package 1.0 # native release build -> build/HanjaKanaMun_1.0__.tar.gz +just windows-deps # Linux only: fetch Windows SDK/CRT (xwin) and raylib.lib +just package-windows 1.0 # cross-compiled Windows build -> build/HanjaKanaMun_1.0_windows_x86_64.zip ``` +The Windows build needs `lld-link`. On macOS, `brew install lld` and set `LLD_LINK`, and point `XWIN_SDK` and +`RAYLIB_WINDOWS` at an xwin splat and Odin's Windows `vendor/raylib/windows` folder (the Linux/macOS Odin releases +don't include `raylib.lib`). + ### Neovim `ols.json` and `odinfmt.json` configure the [ols](https://github.com/DanielGavin/ols) language server and formatter. @@ -22,8 +28,12 @@ Formatting goes through ols (`vim.lsp.buf.format()`), or `just fmt` if `odinfmt` ## CI - `.gitea/workflows/gitea-build.yaml`: check, test and build on pushes and PRs to `main` and `develop`. -- `.gitea/workflows/gitea-release.yaml`: on version branches like `1.0`, packages the Linux build and uploads it to - the Gitea generic package registry (`HanjaKanaMun/`). Needs a `TOKEN` secret with package write access. +- `.gitea/workflows/gitea-release.yaml`: on a version tag like `v1.0`, builds Linux and Windows packages, uploads them + to the Gitea generic package registry (`HanjaKanaMun/1.0`) and creates a Gitea release with both attached. + Needs a `TOKEN` secret with package write access. +- `.gitea/actions/setup`: shared toolchain setup (apt packages, Odin, just). + +Release: `git tag v1.0 && git push origin v1.0` ## Credits diff --git a/justfile b/justfile index 1b466e3..6a3f14f 100644 --- a/justfile +++ b/justfile @@ -1,5 +1,13 @@ name := "HanjaKanaMun" build_dir := "build" +odin_version := "dev-2026-09" +xwin_version := "0.10.0" + +# Windows cross-compilation inputs (see `windows-deps`) +xwin := env("XWIN", build_dir / "xwin") +xwin_sdk := env("XWIN_SDK", build_dir / "xwin-sdk") +raylib_windows := env("RAYLIB_WINDOWS", build_dir / "raylib-windows") +lld_link := env("LLD_LINK", "lld-link") # List available recipes default: @@ -22,17 +30,65 @@ test: mkdir -p {{build_dir}} odin test tests -vet -define:ODIN_TEST_THREADS=1 -out:{{build_dir}}/tests -# Optimized build into build/, with the assets it needs at runtime +# Optimized native build into build//, with the assets it needs at runtime release: - mkdir -p {{build_dir}}/{{name}} - odin build . -vet -o:speed -out:{{build_dir}}/{{name}}/{{name}} - cp -R assets {{build_dir}}/{{name}}/ + mkdir -p {{build_dir}}/{{os()}}/{{name}} + odin build . -vet -o:speed -out:{{build_dir}}/{{os()}}/{{name}}/{{name}} + cp -R assets {{build_dir}}/{{os()}}/{{name}}/ -# Release build packed as build/___.tar.gz +# Native release build packed as build/___.tar.gz package version="dev": release - tar -czf {{build_dir}}/{{name}}_{{version}}_{{os()}}_{{arch()}}.tar.gz -C {{build_dir}} {{name}} + tar -czf {{build_dir}}/{{name}}_{{version}}_{{os()}}_{{arch()}}.tar.gz -C {{build_dir}}/{{os()}} {{name}} @echo "{{build_dir}}/{{name}}_{{version}}_{{os()}}_{{arch()}}.tar.gz" +# Download the Windows SDK + CRT (via xwin, accepting Microsoft's license) and raylib.lib. Linux x86_64 only. +windows-deps: + #!/usr/bin/env bash + set -euo pipefail + mkdir -p {{build_dir}} + if [ ! -x "{{xwin}}" ]; then + curl -fsSL https://github.com/Jake-Shadle/xwin/releases/download/{{xwin_version}}/xwin-{{xwin_version}}-x86_64-unknown-linux-musl.tar.gz \ + | tar -xz -C {{build_dir}} --strip-components=1 xwin-{{xwin_version}}-x86_64-unknown-linux-musl/xwin + fi + if [ ! -d "{{xwin_sdk}}/crt" ]; then + "{{xwin}}" --accept-license --arch x86_64 --cache-dir {{build_dir}}/xwin-cache splat --output "{{xwin_sdk}}" + fi + # Odin's Linux/macOS releases don't ship raylib's Windows library, so take it from the Windows release + if [ ! -f "{{raylib_windows}}/raylib.lib" ]; then + mkdir -p "{{raylib_windows}}" + curl -fsSL -o {{build_dir}}/odin-windows.zip \ + https://github.com/odin-lang/Odin/releases/download/{{odin_version}}/odin-windows-amd64-{{odin_version}}.zip + unzip -o -j {{build_dir}}/odin-windows.zip 'dist/vendor/raylib/windows/raylib.lib' -d "{{raylib_windows}}" + fi + +# Cross-compile a Windows x86_64 build into build/windows/ (run `just windows-deps` first) +windows: + #!/usr/bin/env bash + set -euo pipefail + obj_dir={{build_dir}}/obj-windows + out_dir={{build_dir}}/windows/{{name}} + mkdir -p "$obj_dir" "$out_dir" + rm -f "$obj_dir"/*.obj + odin build . -vet -o:speed -target:windows_amd64 -build-mode:obj -out:"$obj_dir/{{name}}.obj" + # Odin can't link for Windows from other platforms, so link with lld-link. + # The VC runtime is linked statically (no VCRUNTIME140.dll needed); the + # Universal CRT is used from Windows itself (built into Windows 10+). + {{lld_link}} /nologo /machine:x64 /subsystem:windows /entry:mainCRTStartup \ + /out:"$out_dir/{{name}}.exe" "$obj_dir"/*.obj "{{raylib_windows}}/raylib.lib" \ + /libpath:"{{xwin_sdk}}/crt/lib/x86_64" \ + /libpath:"{{xwin_sdk}}/sdk/lib/um/x86_64" \ + /libpath:"{{xwin_sdk}}/sdk/lib/ucrt/x86_64" \ + /nodefaultlib:msvcrt /nodefaultlib:vcruntime /nodefaultlib:libucrt \ + libcmt.lib libvcruntime.lib ucrt.lib \ + kernel32.lib user32.lib gdi32.lib winmm.lib shell32.lib opengl32.lib \ + advapi32.lib bcrypt.lib ntdll.lib synchronization.lib + cp -R assets "$out_dir/" + +# Windows build packed as build/__windows_x86_64.zip +package-windows version="dev": windows + cd {{build_dir}}/windows && zip -qr ../{{name}}_{{version}}_windows_x86_64.zip {{name}} + @echo "{{build_dir}}/{{name}}_{{version}}_windows_x86_64.zip" + # Format all Odin files with odinfmt (settings in odinfmt.json) fmt: odinfmt -w .