From 4d59e96598e36b40005c972c3382eb534a8bf247 Mon Sep 17 00:00:00 2001 From: realmaitreal Date: Fri, 31 Jul 2026 15:27:42 +0200 Subject: [PATCH 1/2] nightly: bundle the engine from the private engine repo into the app --- .github/workflows/nightly.yml | 91 +++++++++++++++++++++++++++++++++++ buildapp.sh | 23 +++++++++ 2 files changed, 114 insertions(+) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index a785a3f..cccb327 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -45,10 +45,101 @@ jobs: fi xcodebuild -version + - name: Fetch the engine payloads from the private engine repo + # Makes a nightly behave like an official release: installer.sh finds the + # engine inside Resources and sets it up offline, instead of building or + # downloading wine itself. + # + # WINE_ENGINE_TOKEN is a fine-grained PAT with Contents: read-only on + # mont127/MacNdCheese-WineEngine-PRIVATE (that repo is private, so the + # workflow's own GITHUB_TOKEN cannot see it). If the secret is absent the + # step is skipped and the build falls back to launcher-only rather than + # failing -- a fork without the secret still gets a working nightly. + id: payload + env: + GH_TOKEN: ${{ secrets.WINE_ENGINE_TOKEN }} + ENGINE_REPO: mont127/MacNdCheese-WineEngine-PRIVATE + run: | + set -euo pipefail + if [ -z "${GH_TOKEN:-}" ]; then + echo "::warning::WINE_ENGINE_TOKEN not set - building launcher-only (no bundled engine)" + echo "bundled=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + mkdir -p payload work + + # Newest release carrying a unified-engine asset. Picked by ASSET NAME, not + # by "latest": the engine repo also publishes mnc-d3d-only releases, and + # engine builds are often prereleases, so `--latest` would pick the wrong one. + ENGINE_TAG="$(gh release list --repo "$ENGINE_REPO" --limit 100 --json tagName \ + --jq '.[].tagName' | while read -r t; do + if gh api "repos/$ENGINE_REPO/releases/tags/$t" \ + --jq '.assets[].name' 2>/dev/null | grep -q 'unified-osx64\.tar\.xz'; then + echo "$t"; break + fi + done)" + [ -n "$ENGINE_TAG" ] || { echo "::error::no unified engine asset found in $ENGINE_REPO"; exit 1; } + echo "engine release: $ENGINE_TAG" + gh release download "$ENGINE_TAG" --repo "$ENGINE_REPO" \ + --pattern '*unified-osx64.tar.xz' --dir work + + # installer.sh does `unzip -d deps/wine-unified`, so the archive contents + # must sit at the ZIP ROOT. The tarball wraps them in wine-unified/, and + # carries mnc-d3d/ which is staged separately from Resources -- so zip from + # inside that dir and leave mnc-d3d out. + tar -xJf work/*unified-osx64.tar.xz -C work + ( cd work/wine-unified && zip -qr -X "$GITHUB_WORKSPACE/payload/wine-unified-bundle.zip" . -x 'mnc-d3d/*' ) + + # The d3d pack ships as its own release so it can be refreshed without + # rebuilding wine. It unpacks to a versioned dir (mnc-d3d-NNNN/), but + # stage_unified_d3d_pack looks for exactly Resources/mnc-d3d. + D3D_TAG="$(gh release list --repo "$ENGINE_REPO" --limit 100 --json tagName \ + --jq '.[].tagName' | while read -r t; do + if gh api "repos/$ENGINE_REPO/releases/tags/$t" \ + --jq '.assets[].name' 2>/dev/null | grep -qx 'mnc-d3d\.tar\.xz'; then + echo "$t"; break + fi + done)" + if [ -n "$D3D_TAG" ]; then + echo "d3d pack release: $D3D_TAG" + gh release download "$D3D_TAG" --repo "$ENGINE_REPO" \ + --pattern 'mnc-d3d.tar.xz' --dir work + mkdir -p work/d3d && tar -xJf work/mnc-d3d.tar.xz -C work/d3d + mv "$(find work/d3d -maxdepth 1 -mindepth 1 -type d | head -1)" payload/mnc-d3d + else + # Fall back to the copy inside the engine tarball. + [ -d work/wine-unified/mnc-d3d ] && cp -R work/wine-unified/mnc-d3d payload/mnc-d3d + fi + + # A d3d pack missing either half of the D3DMetal runtime loads fine and then + # dies on `Failed to dlopen D3DMetal` with no hint a file is missing, so fail + # loudly here instead of shipping it. + for req in d3d11_dxmt.dll dxgi_dxmt.dll libd3dshared.dylib D3DMetal.framework; do + [ -e "payload/mnc-d3d/$req" ] || { echo "::error::d3d pack incomplete: $req missing"; exit 1; } + done + + rm -rf work + echo "bundled=true" >> "$GITHUB_OUTPUT" + du -sh payload/* | sed 's/^/payload: /' + - name: Build universal DMG + env: + MNC_PAYLOAD_DIR: payload run: | chmod +x buildapp.sh bash buildapp.sh universal + ls -la *.dmg + + - name: Fail if the DMG exceeds GitHub's 2 GB asset limit + # A bundled nightly is ~1.9 GB; uploads over 2 GB are rejected outright and + # the failure surfaces late and confusingly, so check it up front. + run: | + SZ=$(stat -f%z "MacNdCheese Launcher.dmg" 2>/dev/null || echo 0) + echo "DMG size: $((SZ/1048576)) MB" + if [ "$SZ" -gt 2147483648 ]; then + echo "::error::DMG is $((SZ/1048576)) MB, over GitHub's 2048 MB asset limit" + exit 1 + fi - name: Compute tag and release notes id: tag diff --git a/buildapp.sh b/buildapp.sh index 20e6f00..6476225 100644 --- a/buildapp.sh +++ b/buildapp.sh @@ -123,6 +123,29 @@ for img in Steam.png Wine.png Setting.png Add.png icon.png; do fi done +# Engine payloads (wine-unified-bundle.zip, mnc-d3d/, redist/, ...). An official +# release ships these inside Resources -- that is what makes installer.sh able to +# set the engine up offline instead of building or downloading wine itself. They +# are far too large for git, so they are supplied out of band: point +# MNC_PAYLOAD_DIR at a directory containing them (the nightly workflow fills it +# from the engine repo's releases). +# +# Whatever is in that directory is copied verbatim, so a payload set can grow +# without touching this script. Absent = a launcher-only build, which is the +# pre-existing behaviour and still perfectly usable -- installer.sh just falls +# back to its other sources. +PAYLOAD_DIR="${MNC_PAYLOAD_DIR:-payload}" +if [ -d "$PAYLOAD_DIR" ] && [ -n "$(ls -A "$PAYLOAD_DIR" 2>/dev/null)" ]; then + echo "Bundling engine payloads from $PAYLOAD_DIR:" + for item in "$PAYLOAD_DIR"/*; do + [ -e "$item" ] || continue + cp -R "$item" "$RESOURCES/" + echo " + $(basename "$item") ($(du -sh "$item" 2>/dev/null | cut -f1))" + done +else + echo "No engine payloads ($PAYLOAD_DIR absent/empty) — building launcher-only." +fi + # Extract App Intents metadata so Siri/Apple Intelligence can discover shortcuts. # App Intents definitions don't vary by CPU arch, so for a universal build one # representative triple (arm64) is enough — this only affects Siri phrase From 9ee378cc2e91b156d2d4a358ba030f1ca09368a4 Mon Sep 17 00:00:00 2001 From: realmaitreal Date: Fri, 31 Jul 2026 15:40:50 +0200 Subject: [PATCH 2/2] installer: accept a .tar.xz engine bundle, and use the d3d pack it ships --- .github/workflows/nightly.yml | 86 +++++++++++++---------------------- installer.sh | 42 +++++++++++++---- 2 files changed, 65 insertions(+), 63 deletions(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index cccb327..1795d39 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -45,16 +45,16 @@ jobs: fi xcodebuild -version - - name: Fetch the engine payloads from the private engine repo + - name: Fetch the engine bundle from the private engine repo # Makes a nightly behave like an official release: installer.sh finds the - # engine inside Resources and sets it up offline, instead of building or - # downloading wine itself. + # engine inside Resources and sets it up offline instead of building or + # downloading wine itself. Installing = replacing deps/wine-unified + # wholesale, which is what installer.sh already does (rm -rf then extract). # - # WINE_ENGINE_TOKEN is a fine-grained PAT with Contents: read-only on - # mont127/MacNdCheese-WineEngine-PRIVATE (that repo is private, so the - # workflow's own GITHUB_TOKEN cannot see it). If the secret is absent the - # step is skipped and the build falls back to launcher-only rather than - # failing -- a fork without the secret still gets a working nightly. + # WINE_ENGINE_TOKEN is a fine-grained PAT with Contents: read-only on the + # engine repo -- it is private, so the workflow's own GITHUB_TOKEN cannot + # see it. Absent secret = launcher-only build rather than a hard failure, + # so a fork without the secret still produces a usable nightly. id: payload env: GH_TOKEN: ${{ secrets.WINE_ENGINE_TOKEN }} @@ -66,61 +66,39 @@ jobs: echo "bundled=false" >> "$GITHUB_OUTPUT" exit 0 fi - mkdir -p payload work + mkdir -p payload - # Newest release carrying a unified-engine asset. Picked by ASSET NAME, not - # by "latest": the engine repo also publishes mnc-d3d-only releases, and - # engine builds are often prereleases, so `--latest` would pick the wrong one. - ENGINE_TAG="$(gh release list --repo "$ENGINE_REPO" --limit 100 --json tagName \ + # Newest release carrying an engine asset, chosen by ASSET NAME rather than + # `--latest`: engine builds are often prereleases and the repo also publishes + # d3d-pack-only releases, either of which would make --latest pick the wrong one. + TAG="$(gh release list --repo "$ENGINE_REPO" --limit 100 --json tagName \ --jq '.[].tagName' | while read -r t; do if gh api "repos/$ENGINE_REPO/releases/tags/$t" \ --jq '.assets[].name' 2>/dev/null | grep -q 'unified-osx64\.tar\.xz'; then echo "$t"; break fi done)" - [ -n "$ENGINE_TAG" ] || { echo "::error::no unified engine asset found in $ENGINE_REPO"; exit 1; } - echo "engine release: $ENGINE_TAG" - gh release download "$ENGINE_TAG" --repo "$ENGINE_REPO" \ - --pattern '*unified-osx64.tar.xz' --dir work - - # installer.sh does `unzip -d deps/wine-unified`, so the archive contents - # must sit at the ZIP ROOT. The tarball wraps them in wine-unified/, and - # carries mnc-d3d/ which is staged separately from Resources -- so zip from - # inside that dir and leave mnc-d3d out. - tar -xJf work/*unified-osx64.tar.xz -C work - ( cd work/wine-unified && zip -qr -X "$GITHUB_WORKSPACE/payload/wine-unified-bundle.zip" . -x 'mnc-d3d/*' ) - - # The d3d pack ships as its own release so it can be refreshed without - # rebuilding wine. It unpacks to a versioned dir (mnc-d3d-NNNN/), but - # stage_unified_d3d_pack looks for exactly Resources/mnc-d3d. - D3D_TAG="$(gh release list --repo "$ENGINE_REPO" --limit 100 --json tagName \ - --jq '.[].tagName' | while read -r t; do - if gh api "repos/$ENGINE_REPO/releases/tags/$t" \ - --jq '.assets[].name' 2>/dev/null | grep -qx 'mnc-d3d\.tar\.xz'; then - echo "$t"; break - fi - done)" - if [ -n "$D3D_TAG" ]; then - echo "d3d pack release: $D3D_TAG" - gh release download "$D3D_TAG" --repo "$ENGINE_REPO" \ - --pattern 'mnc-d3d.tar.xz' --dir work - mkdir -p work/d3d && tar -xJf work/mnc-d3d.tar.xz -C work/d3d - mv "$(find work/d3d -maxdepth 1 -mindepth 1 -type d | head -1)" payload/mnc-d3d - else - # Fall back to the copy inside the engine tarball. - [ -d work/wine-unified/mnc-d3d ] && cp -R work/wine-unified/mnc-d3d payload/mnc-d3d - fi - - # A d3d pack missing either half of the D3DMetal runtime loads fine and then - # dies on `Failed to dlopen D3DMetal` with no hint a file is missing, so fail - # loudly here instead of shipping it. - for req in d3d11_dxmt.dll dxgi_dxmt.dll libd3dshared.dylib D3DMetal.framework; do - [ -e "payload/mnc-d3d/$req" ] || { echo "::error::d3d pack incomplete: $req missing"; exit 1; } + [ -n "$TAG" ] || { echo "::error::no unified engine asset found in $ENGINE_REPO"; exit 1; } + echo "engine release: $TAG" + + gh release download "$TAG" --repo "$ENGINE_REPO" \ + --pattern '*unified-osx64.tar.xz' --dir payload + mv payload/*unified-osx64.tar.xz payload/wine-unified-bundle.tar.xz + + # The engine bundle carries mnc-d3d/ itself, so nothing else to fetch. Verify + # it is complete: a pack missing either half of the D3DMetal runtime loads + # fine and then dies on "Failed to dlopen D3DMetal" with no hint a file is + # missing, so fail here rather than shipping it. + for req in mnc-d3d/d3d11_dxmt.dll mnc-d3d/dxgi_dxmt.dll \ + mnc-d3d/libd3dshared.dylib mnc-d3d/D3DMetal.framework \ + loader/wine server/wineserver; do + tar -tJf payload/wine-unified-bundle.tar.xz "wine-unified/$req" >/dev/null 2>&1 \ + || { echo "::error::engine bundle incomplete: $req missing"; exit 1; } done - rm -rf work - echo "bundled=true" >> "$GITHUB_OUTPUT" - du -sh payload/* | sed 's/^/payload: /' + echo "engine_tag=$TAG" >> "$GITHUB_OUTPUT" + echo "bundled=true" >> "$GITHUB_OUTPUT" + ls -lh payload/ - name: Build universal DMG env: diff --git a/installer.sh b/installer.sh index 4de54f8..80a2009 100644 --- a/installer.sh +++ b/installer.sh @@ -2233,13 +2233,21 @@ locate_wine_unified_bundle() { script_path="$0" case "$script_path" in /*) ;; *) script_path="$PWD/$script_path" ;; esac script_dir="$(cd "$(dirname "$script_path")" 2>/dev/null && pwd)" || script_dir="" + # .tar.xz is what the engine repo publishes (and what the nightly bundles); + # .zip is the historical hand-made bundle. Both are accepted. candidates=" ${WINE_UNIFIED_BUNDLE_PATH:-} +${RESOURCES_DIR:-}/wine-unified-bundle.tar.xz ${RESOURCES_DIR:-}/wine-unified-bundle.zip +${script_dir}/wine-unified-bundle.tar.xz ${script_dir}/wine-unified-bundle.zip +${script_dir}/../Resources/wine-unified-bundle.tar.xz ${script_dir}/../Resources/wine-unified-bundle.zip +${script_dir}/../../Resources/wine-unified-bundle.tar.xz ${script_dir}/../../Resources/wine-unified-bundle.zip +$HOME/macndcheese/wine-unified-bundle.tar.xz $HOME/macndcheese/wine-unified-bundle.zip +$HOME/Library/Application Support/MacNCheese/wine-unified-bundle.tar.xz $HOME/Library/Application Support/MacNCheese/wine-unified-bundle.zip " while IFS= read -r c; do @@ -2250,7 +2258,7 @@ $candidates EOF for root in /Applications "$HOME/Applications" "$HOME/Downloads"; do [ -d "$root" ] || continue - found="$(find "$root" -maxdepth 5 -name 'wine-unified-bundle.zip' -type f 2>/dev/null | head -n1)" + found="$(find "$root" -maxdepth 5 \( -name 'wine-unified-bundle.tar.xz' -o -name 'wine-unified-bundle.zip' \) -type f 2>/dev/null | head -n1)" [ -n "$found" ] && [ -f "$found" ] && { printf '%s' "$found"; return 0; } done return 1 @@ -2309,16 +2317,32 @@ install_wine_unified() { echo "Using unified wine bundle: $bundle" rm -rf "$dst" mkdir -p "$dst" - if command -v unzip >/dev/null 2>&1; then - unzip -q "$bundle" -d "$dst" || { echo "Failed to unzip unified wine bundle"; exit 1; } - elif [ -x "$SEVENZ_BIN" ]; then - "$SEVENZ_BIN" x -y -o"$dst" "$bundle" >/dev/null || { echo "Failed to extract unified wine bundle"; exit 1; } - else - echo "Neither unzip nor 7z available to extract the bundle"; exit 1 - fi + case "$bundle" in + *.tar.xz|*.txz) + # The tarball wraps everything in one top-level dir (wine-unified/), but + # $dst IS that dir -- strip a level or it ends up nested twice. + tar -xJf "$bundle" -C "$dst" --strip-components=1 \ + || { echo "Failed to untar unified wine bundle"; exit 1; } + ;; + *) + if command -v unzip >/dev/null 2>&1; then + unzip -q "$bundle" -d "$dst" || { echo "Failed to unzip unified wine bundle"; exit 1; } + elif [ -x "$SEVENZ_BIN" ]; then + "$SEVENZ_BIN" x -y -o"$dst" "$bundle" >/dev/null || { echo "Failed to extract unified wine bundle"; exit 1; } + else + echo "Neither unzip nor 7z available to extract the bundle"; exit 1 + fi + ;; + esac find "$dst" -name 'wine' -type f -exec chmod +x {} \; 2>/dev/null || true xattr -dr com.apple.quarantine "$dst" 2>/dev/null || true - stage_unified_d3d_pack "$dst" + # The engine bundle now ships mnc-d3d/ itself; only stage from Resources when + # it didn't (older hand-made .zip bundles). + if [ -f "$dst/mnc-d3d/d3d11.dll" ]; then + echo "install_wine_unified: d3d pack came with the bundle ($(ls "$dst/mnc-d3d" 2>/dev/null | wc -l | tr -d ' ') entries)" + else + stage_unified_d3d_pack "$dst" + fi stage_redist_pack sign_unified_wine "$dst" echo "install_wine_unified: done ($(du -sh "$dst" 2>/dev/null | cut -f1))"