diff --git a/.github/workflows/installer.yml b/.github/workflows/installer.yml
index e264193d506..1eeecbfc3ac 100644
--- a/.github/workflows/installer.yml
+++ b/.github/workflows/installer.yml
@@ -12,6 +12,7 @@ env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
jobs:
release:
+ if: github.event_name == 'workflow_dispatch' || startsWith(github.event.release.tag_name, 'v')
runs-on: windows-latest
steps:
- name: Checkout
diff --git a/.github/workflows/macos-release.yml b/.github/workflows/macos-release.yml
new file mode 100644
index 00000000000..18eb9ea7119
--- /dev/null
+++ b/.github/workflows/macos-release.yml
@@ -0,0 +1,237 @@
+name: Publish experimental macOS RC
+run-name: Publish macOS v${{ inputs.release_version }}-rc.${{ inputs.rc_number }}
+
+on:
+ workflow_dispatch:
+ inputs:
+ release_version:
+ description: Upstream Path of Building version (X.Y.Z)
+ required: true
+ default: '2.66.2'
+ type: string
+ rc_number:
+ description: macOS release candidate number
+ required: true
+ default: '1'
+ type: string
+ simplegraphic_tag:
+ description: PathOfBuilding-SimpleGraphic prerelease tag
+ required: true
+ default: issue-9-smoke-20260730
+ type: string
+ simplegraphic_commit:
+ description: Full commit SHA for the SimpleGraphic prerelease
+ required: true
+ default: 98e98efcc747fea5ee96dd933de51685887db417
+ type: string
+ simplegraphic_sha256:
+ description: SHA-256 for SimpleGraphicSmoke-macos13-arm64.zip
+ required: true
+ default: 4eb99b5d371243ea11ab02de891d6e9624f809d64e8dfc6a3cd760fc9b6a06c2
+ type: string
+ simplegraphic_windows_run_id:
+ description: Successful Windows CI run ID for the SimpleGraphic commit
+ required: true
+ type: string
+
+permissions:
+ contents: read
+
+jobs:
+ tests:
+ runs-on: ubuntu-latest
+ container: ghcr.io/pathofbuildingcommunity/pathofbuilding-tests:latest
+ steps:
+ - name: Checkout candidate
+ uses: actions/checkout@v4
+ - name: Run application tests
+ run: busted --lua=luajit
+
+ modcache:
+ runs-on: ubuntu-latest
+ container: ghcr.io/pathofbuildingcommunity/pathofbuilding-tests:latest
+ steps:
+ - name: Install git
+ run: apk add git
+ - name: Checkout candidate
+ uses: actions/checkout@v4
+ - name: Regenerate ModCache
+ env:
+ LUA_PATH: ../runtime/lua/?.lua;../runtime/lua/?/init.lua
+ REGENERATE_MOD_CACHE: 1
+ working-directory: src
+ run: luajit HeadlessWrapper.lua
+ - name: Check ModCache
+ run: |
+ git config --global --add safe.directory "$GITHUB_WORKSPACE"
+ git diff --exit-code src/Data/ModCache.lua
+
+ publish:
+ needs: [tests, modcache]
+ runs-on: macos-15
+ permissions:
+ contents: write
+ env:
+ GH_TOKEN: ${{ github.token }}
+ RELEASE_VERSION: ${{ inputs.release_version }}
+ RC_NUMBER: ${{ inputs.rc_number }}
+ SIMPLEGRAPHIC_TAG: ${{ inputs.simplegraphic_tag }}
+ SIMPLEGRAPHIC_COMMIT: ${{ inputs.simplegraphic_commit }}
+ SIMPLEGRAPHIC_SHA256: ${{ inputs.simplegraphic_sha256 }}
+ SIMPLEGRAPHIC_WINDOWS_RUN_ID: ${{ inputs.simplegraphic_windows_run_id }}
+ steps:
+ - name: Checkout candidate
+ uses: actions/checkout@v4
+
+ - name: Validate release inputs
+ run: |
+ if ! printf '%s\n' "$RELEASE_VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
+ printf 'release_version must use X.Y.Z: %s\n' "$RELEASE_VERSION" >&2
+ exit 2
+ fi
+ if ! printf '%s\n' "$RC_NUMBER" | grep -Eq '^[1-9][0-9]*$'; then
+ printf 'rc_number must be a positive integer: %s\n' "$RC_NUMBER" >&2
+ exit 2
+ fi
+ if ! printf '%s\n' "$SIMPLEGRAPHIC_TAG" | grep -Eq '^[A-Za-z0-9][A-Za-z0-9._-]*$'; then
+ printf 'invalid SimpleGraphic release tag: %s\n' "$SIMPLEGRAPHIC_TAG" >&2
+ exit 2
+ fi
+ if ! printf '%s\n' "$SIMPLEGRAPHIC_COMMIT" | grep -Eq '^[0-9a-f]{40}$'; then
+ printf 'simplegraphic_commit must be a full commit SHA\n' >&2
+ exit 2
+ fi
+ if ! printf '%s\n' "$SIMPLEGRAPHIC_SHA256" | grep -Eq '^[0-9a-f]{64}$'; then
+ printf 'simplegraphic_sha256 must be a SHA-256 digest\n' >&2
+ exit 2
+ fi
+ if ! printf '%s\n' "$SIMPLEGRAPHIC_WINDOWS_RUN_ID" | grep -Eq '^[1-9][0-9]*$'; then
+ printf 'simplegraphic_windows_run_id must be a positive integer\n' >&2
+ exit 2
+ fi
+
+ rc_version="$RELEASE_VERSION-rc.$RC_NUMBER"
+ tag_name="macos-v$rc_version"
+ source_version=$(grep -m 1 -o 'Version number="[^"]*"' manifest.xml | cut -d '"' -f 2)
+ if [ "$source_version" != "$RELEASE_VERSION" ]; then
+ printf 'release version %s does not match manifest version %s\n' "$RELEASE_VERSION" "$source_version" >&2
+ exit 1
+ fi
+ if gh release view "$tag_name" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
+ printf 'release already exists: %s\n' "$tag_name" >&2
+ exit 1
+ fi
+ if git ls-remote --exit-code --tags origin "refs/tags/$tag_name" >/dev/null 2>&1; then
+ printf 'tag already exists: %s\n' "$tag_name" >&2
+ exit 1
+ fi
+
+ runtime_repo="${GITHUB_REPOSITORY_OWNER}/PathOfBuilding-SimpleGraphic"
+ runtime_tag=$(gh api "repos/$runtime_repo/git/ref/tags/$SIMPLEGRAPHIC_TAG")
+ runtime_tag_type=$(printf '%s' "$runtime_tag" | jq -r '.object.type')
+ runtime_tag_commit=$(printf '%s' "$runtime_tag" | jq -r '.object.sha')
+ if [ "$runtime_tag_type" != 'commit' ]; then
+ printf 'SimpleGraphic release must use a lightweight commit tag, found %s\n' "$runtime_tag_type" >&2
+ exit 1
+ fi
+ if [ "$runtime_tag_commit" != "$SIMPLEGRAPHIC_COMMIT" ]; then
+ printf 'SimpleGraphic tag resolves to %s, expected %s\n' "$runtime_tag_commit" "$SIMPLEGRAPHIC_COMMIT" >&2
+ exit 1
+ fi
+ windows_run=$(gh api "repos/$runtime_repo/actions/runs/$SIMPLEGRAPHIC_WINDOWS_RUN_ID")
+ windows_run_commit=$(printf '%s' "$windows_run" | jq -r '.head_sha')
+ windows_run_result=$(printf '%s' "$windows_run" | jq -r '.status + "/" + (.conclusion // "")')
+ windows_run_path=$(printf '%s' "$windows_run" | jq -r '.path')
+ if [ "$windows_run_commit" != "$SIMPLEGRAPHIC_COMMIT" ] \
+ || [ "$windows_run_result" != 'completed/success' ] \
+ || [ "$windows_run_path" != '.github/workflows/main.yml' ]; then
+ printf 'SimpleGraphic Windows workflow is not successful for %s: %s at %s from %s\n' "$SIMPLEGRAPHIC_COMMIT" "$windows_run_result" "$windows_run_commit" "$windows_run_path" >&2
+ exit 1
+ fi
+ printf 'RC_VERSION=%s\n' "$rc_version" >> "$GITHUB_ENV"
+ printf 'TAG_NAME=%s\n' "$tag_name" >> "$GITHUB_ENV"
+
+ - name: Download SimpleGraphic runtime
+ run: |
+ mkdir -p build-macos/runtime
+ gh release download "$SIMPLEGRAPHIC_TAG" \
+ --repo "${GITHUB_REPOSITORY_OWNER}/PathOfBuilding-SimpleGraphic" \
+ --pattern 'SimpleGraphicSmoke-macos13-arm64.zip' \
+ --dir build-macos/runtime
+ printf '%s %s\n' \
+ "$SIMPLEGRAPHIC_SHA256" \
+ build-macos/runtime/SimpleGraphicSmoke-macos13-arm64.zip \
+ | shasum -a 256 -c -
+ ditto -x -k \
+ build-macos/runtime/SimpleGraphicSmoke-macos13-arm64.zip \
+ build-macos/runtime
+ test -d build-macos/runtime/SimpleGraphicSmoke.app
+
+ - name: Run SimpleGraphic acceptance smoke
+ run: |
+ build-macos/runtime/SimpleGraphicSmoke.app/Contents/MacOS/SimpleGraphicSmoke \
+ > build-macos/runtime-acceptance.log 2>&1
+
+ - name: Build and validate release archive
+ run: |
+ macos/package-release.sh \
+ build-macos/runtime/SimpleGraphicSmoke.app \
+ "$RC_VERSION" \
+ build-macos/release
+
+ - name: Prepare release notes
+ run: |
+ archive="PathOfBuilding-macOS-arm64-v$RC_VERSION.zip"
+ checksum=$(cut -d ' ' -f 1 "build-macos/release/$archive.sha256")
+ runner_version=$(sw_vers -productVersion)
+ windows_run_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY_OWNER/PathOfBuilding-SimpleGraphic/actions/runs/$SIMPLEGRAPHIC_WINDOWS_RUN_ID"
+ {
+ printf '%s\n' '# Experimental macOS release candidate'
+ printf '\n%s\n' '**Unsigned, experimental, and published from the `rodgons/PathOfBuilding` fork. This is not an official upstream Path of Building release.**'
+ printf '\nPath of Building %s macOS RC %s for Apple Silicon (macOS 13+).\n' "$RELEASE_VERSION" "$RC_NUMBER"
+ printf '\n%s\n' '## Provenance'
+ printf '\n- Application commit: [`%s`](%s/%s/commit/%s)\n' "$GITHUB_SHA" "$GITHUB_SERVER_URL" "$GITHUB_REPOSITORY" "$GITHUB_SHA"
+ printf -- '- SimpleGraphic runtime: [`%s`](https://github.com/%s/PathOfBuilding-SimpleGraphic/commit/%s), release `%s`, SHA-256 `%s`\n' "$SIMPLEGRAPHIC_COMMIT" "$GITHUB_REPOSITORY_OWNER" "$SIMPLEGRAPHIC_COMMIT" "$SIMPLEGRAPHIC_TAG" "$SIMPLEGRAPHIC_SHA256"
+ printf -- '- SimpleGraphic Windows CI: [successful run %s](%s)\n' "$SIMPLEGRAPHIC_WINDOWS_RUN_ID" "$windows_run_url"
+ printf -- '- SHA-256: `%s`\n' "$checksum"
+ printf '\n%s\n' '## Validation status'
+ printf '\nAutomated application tests, ModCache regeneration, bundle inventory, arm64/macOS 13 deployment-target and native dependency checks, archive extraction, signature verification, and unattended startup smoke passed on a GitHub-hosted Apple Silicon runner (macOS %s).\n' "$runner_version"
+ printf '\nPhysical Apple Silicon macOS 13 workflow validation is pending in [issue #12](https://github.com/%s/issues/12).\n' "$GITHUB_REPOSITORY"
+ printf '\n%s\n' '## Install'
+ printf '\n1. Download `%s` and `%s.sha256`.\n' "$archive" "$archive"
+ printf '%s\n' '2. Verify with `shasum -a 256 -c PathOfBuilding-macOS-arm64-*.zip.sha256`.'
+ printf '%s\n' '3. Extract the zip and move `Path of Building (macOS RC).app` to Applications.'
+ printf '%s\n' '4. Control-click the app and choose **Open**. If macOS still blocks it, use **System Settings > Privacy & Security > Open Anyway**.'
+ printf '\nTo update manually, replace the `.app`; builds and settings remain in `~/Library/Application Support/Path of Building/`.\n'
+ printf '\n%s\n' '## Known limitations'
+ printf '\n%s\n' '- The app is ad-hoc signed, not Developer ID signed or notarized.'
+ printf '%s\n' "- In-app self-update is disabled; **Get macOS RC Updates** opens this fork's releases."
+ printf '%s\n' '- SimpleGraphic runtime validation on physical macOS 13 and the full planner workflow checklist remain pending.'
+ printf '%s\n' '- Native host logs are currently suppressed instead of being redirected to `~/Library/Logs/Path of Building/`.'
+ } > build-macos/release/release-notes.md
+
+ - name: Publish prerelease
+ run: |
+ archive="PathOfBuilding-macOS-arm64-v$RC_VERSION.zip"
+ gh release create "$TAG_NAME" \
+ "build-macos/release/$archive" \
+ "build-macos/release/$archive.sha256" \
+ "build-macos/release/automated-validation.log" \
+ "build-macos/runtime-acceptance.log" \
+ --repo "$GITHUB_REPOSITORY" \
+ --target "$GITHUB_SHA" \
+ --title "Experimental macOS RC $RC_NUMBER for Path of Building $RELEASE_VERSION" \
+ --notes-file build-macos/release/release-notes.md \
+ --prerelease
+
+ - name: Upload validation evidence
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: macos-rc-evidence-v${{ inputs.release_version }}-rc.${{ inputs.rc_number }}
+ if-no-files-found: ignore
+ path: |
+ build-macos/release/*.sha256
+ build-macos/release/automated-validation.log
+ build-macos/release/release-notes.md
+ build-macos/runtime-acceptance.log
diff --git a/.gitignore b/.gitignore
index 704714139fd..e2800b9d559 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,6 +21,7 @@ src/luacov.stats.out
# Release
manifest-updated.xml
+build-macos/
# GGPK Export
src/Export/ggpk/metadata/
@@ -38,4 +39,4 @@ src/Data/TimelessJewelData/*.bin
runtime/imgui.ini
runtime/SimpleGraphic/SimpleGraphic.log
-src/poe_api_response.json
\ No newline at end of file
+src/poe_api_response.json
diff --git a/macos/Info.plist.in b/macos/Info.plist.in
new file mode 100644
index 00000000000..1abfc084ab5
--- /dev/null
+++ b/macos/Info.plist.in
@@ -0,0 +1,29 @@
+
+
+
+
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleDisplayName
+ Path of Building (macOS RC)
+ CFBundleExecutable
+ Path of Building
+ CFBundleIdentifier
+ com.github.rodgons.PathOfBuildingMacOSRC
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ Path of Building (macOS RC)
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ @BUILD_VERSION@
+ CFBundleVersion
+ @BUILD_VERSION@
+ LSMinimumSystemVersion
+ 13.0
+ NSHighResolutionCapable
+
+
+
diff --git a/macos/README.md b/macos/README.md
new file mode 100644
index 00000000000..72dbbe4d2a6
--- /dev/null
+++ b/macos/README.md
@@ -0,0 +1,111 @@
+# Local macOS app integration
+
+
+This directory assembles the existing Path of Building Lua application with the
+Apple Silicon SimpleGraphic runtime produced by issue 9. It supports both local
+integration and the fork's experimental macOS release-candidate channel.
+
+The integration consists of:
+
+- `src/Launch.lua`, `src/Modules/Main.lua`, and `src/Modules/Build.lua`: external-update policy and unattended startup smoke mode.
+- `macos/launcher.c`: Finder-safe native entrypoint and bundle path setup.
+- `macos/Info.plist.in`: temporary RC bundle identity and macOS metadata.
+- `macos/build-app.sh`: local bundle assembly from the issue 9 runtime.
+- `macos/package-release.sh`: release zip, checksum, extraction, inventory,
+ signature, and startup-smoke validation.
+- `macos/verify-app.sh`: layout, policy, architecture, deployment target, dependency, immutability, and signature checks.
+- `spec/System/TestUpdatePolicy_spec.lua`: external-update behavior coverage.
+- `.github/workflows/macos-release.yml`: gated prerelease publication from an
+ Apple Silicon GitHub-hosted runner.
+- `.gitignore`: excludes local assembled bundles.
+
+## Build
+
+Download and unzip `SimpleGraphicSmoke-macos13-arm64.zip` from the issue 9
+prerelease, then run:
+
+```sh
+macos/build-app.sh \
+ "/path/to/SimpleGraphicSmoke.app" \
+ "build-macos/Path of Building (macOS RC).app" \
+ "2.66.2-rc.1"
+```
+
+The script compiles the PoB launcher for arm64/macOS 13, copies the native
+runtime and portable Lua runtime into the agreed bundle layout, marks the app as
+an installed external-update build, verifies the Mach-O files, and ad-hoc signs
+the bundle.
+
+To produce the same archive, checksum, and validation evidence as the release
+workflow, run:
+
+```sh
+macos/package-release.sh \
+ "/path/to/SimpleGraphicSmoke.app" \
+ "2.66.2-rc.1" \
+ "build-macos/release"
+```
+
+Launch from Finder or from a terminal:
+
+```sh
+open "build-macos/Path of Building (macOS RC).app"
+```
+
+For an unattended startup check that exits after application initialization:
+
+```sh
+POB_MACOS_SMOKE=1 \
+ "build-macos/Path of Building (macOS RC).app/Contents/MacOS/Path of Building"
+```
+
+Durable user data is written under
+`~/Library/Application Support/Path of Building/`. The app does not contain or
+run the Windows updater; **Get macOS RC Updates** opens the fork release page.
+
+## Publish an experimental release candidate
+
+The **Publish experimental macOS RC** workflow is manual and must run from the
+exact candidate ref. It accepts the upstream application version, RC number,
+and versioned SimpleGraphic prerelease tag. For example:
+
+```sh
+gh workflow run macos-release.yml \
+ --ref dev \
+ -f release_version=2.66.2 \
+ -f rc_number=1 \
+ -f simplegraphic_tag=issue-9-smoke-20260730 \
+ -f simplegraphic_commit=98e98efcc747fea5ee96dd933de51685887db417 \
+ -f simplegraphic_sha256=4eb99b5d371243ea11ab02de891d6e9624f809d64e8dfc6a3cd760fc9b6a06c2 \
+ -f simplegraphic_windows_run_id=WINDOWS_RUN_ID
+```
+
+The Windows run ID must identify a successful SimpleGraphic `Build DLL` workflow
+run for the exact runtime commit. The workflow verifies that run, the lightweight
+runtime tag and digest, the source manifest version, the runtime acceptance
+smoke, the Lua suite, and ModCache before packaging. It then
+publishes `macos-v2.66.2-rc.1` as an experimental prerelease with the zipped
+app, SHA-256 file, automated validation logs, source/runtime provenance,
+Gatekeeper instructions, and known limitations. The same evidence is retained
+as a workflow artifact. The normal Windows installer workflow ignores these
+`macos-*` prereleases.
+
+Retrieve the release assets and workflow evidence with:
+
+```sh
+gh release download macos-v2.66.2-rc.1 \
+ --pattern 'PathOfBuilding-macOS-arm64-v2.66.2-rc.1*'
+gh run download RUN_ID \
+ --name macos-rc-evidence-v2.66.2-rc.1
+```
+
+## Current blockers
+
+The issue 9 runtime still needs physical macOS 13 and Windows CI validation.
+Clipboard, URL opening, screenshots, HTTPS trust, texture fixtures, and Retina
+cursor hit-testing also remain outside the representative runtime smoke proof.
+The native host currently reports the bundle Resources directory from
+`GetRuntimePath()`; in-app updates are disabled, so this does not block launch,
+but the host should report Frameworks before broader runtime-path use. Native
+SimpleGraphic config and log files are currently suppressed by the read-only
+bundle rather than redirected to `~/Library/Logs/Path of Building/`.
diff --git a/macos/build-app.sh b/macos/build-app.sh
new file mode 100755
index 00000000000..a4134486c75
--- /dev/null
+++ b/macos/build-app.sh
@@ -0,0 +1,78 @@
+#!/bin/sh
+# cspell:ignore CDPATH xcrun codesign
+set -eu
+
+if [ "$#" -lt 1 ] || [ "$#" -gt 3 ]; then
+ echo "usage: $0 [output.app] [version]" >&2
+ exit 2
+fi
+
+script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
+repo_dir=$(dirname "$script_dir")
+runtime_app=$1
+output_app=${2:-"$repo_dir/build-macos/Path of Building (macOS RC).app"}
+version=${3:-"2.66.2-rc.1"}
+runtime_contents="$runtime_app/Contents"
+contents="$output_app/Contents"
+frameworks="$contents/Frameworks"
+resources="$contents/Resources"
+
+if [ ! -f "$runtime_contents/Frameworks/libSimpleGraphic.dylib" ]; then
+ echo "SimpleGraphic runtime not found in: $runtime_app" >&2
+ exit 1
+fi
+case "$output_app" in
+ *.app) ;;
+ *) echo "output path must end in .app" >&2; exit 2 ;;
+esac
+
+if [ -e "$output_app" ]; then
+ chmod -R u+w "$output_app"
+ rm -rf "$output_app"
+fi
+mkdir -p "$contents/MacOS" "$frameworks/lcurl" "$frameworks/socket" "$resources/src" "$resources/runtime"
+cp -R "$runtime_contents/Frameworks/." "$frameworks/"
+cp "$runtime_contents/Resources/lcurl.so" "$frameworks/lcurl.so"
+cp "$runtime_contents/Resources/lcurl/safe.so" "$frameworks/lcurl/safe.so"
+cp "$runtime_contents/Resources/lua-utf8.so" "$frameworks/lua-utf8.so"
+cp "$runtime_contents/Resources/lzip.so" "$frameworks/lzip.so"
+cp "$runtime_contents/Resources/socket/core.so" "$frameworks/socket/core.so"
+cp -R "$repo_dir/runtime/lua" "$resources/runtime/lua"
+cp -R "$repo_dir/runtime/SimpleGraphic" "$resources/runtime/SimpleGraphic"
+
+rsync -a \
+ --exclude "Export/" \
+ --exclude "Builds/" \
+ --exclude "HeadlessWrapper.lua" \
+ --exclude "LaunchInstall.lua" \
+ --exclude "UpdateApply.lua" \
+ --exclude "UpdateCheck.lua" \
+ --exclude "first.run" \
+ "$repo_dir/src/" "$resources/src/"
+cp "$repo_dir/changelog.txt" "$repo_dir/help.txt" "$repo_dir/LICENSE.md" "$resources/src/"
+: > "$resources/src/installed.cfg"
+ln -s ../runtime/SimpleGraphic "$resources/src/SimpleGraphic"
+ln -s runtime/SimpleGraphic "$resources/SimpleGraphic"
+
+build_version=$(printf '%s' "$version" | sed 's/[^0-9.].*$//')
+escaped_build_version=$(printf '%s' "$build_version" | sed 's/[&|]/\\&/g')
+sed "s|@BUILD_VERSION@|$escaped_build_version|g" "$script_dir/Info.plist.in" > "$contents/Info.plist"
+cat > "$resources/manifest.xml" <
+
+
+
+EOF
+
+xcrun --sdk macosx clang \
+ -arch arm64 \
+ -mmacosx-version-min=13.0 \
+ "$script_dir/launcher.c" \
+ "$frameworks/libSimpleGraphic.dylib" \
+ -Wl,-rpath,@executable_path/../Frameworks \
+ -o "$contents/MacOS/Path of Building"
+
+codesign --force --deep --sign - "$output_app"
+chmod -R a-w "$output_app"
+"$script_dir/verify-app.sh" "$output_app"
+echo "$output_app"
diff --git a/macos/launcher.c b/macos/launcher.c
new file mode 100644
index 00000000000..541a0039ad2
--- /dev/null
+++ b/macos/launcher.c
@@ -0,0 +1,70 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+extern int RunLuaFileAsWin(int argc, char **argv);
+
+int main(int argc, char **argv)
+{
+ char executablePath[PATH_MAX];
+ uint32_t executablePathSize = sizeof(executablePath);
+ if (_NSGetExecutablePath(executablePath, &executablePathSize) != 0) {
+ fprintf(stderr, "Could not resolve the application executable path\n");
+ return 1;
+ }
+
+ char resolvedExecutablePath[PATH_MAX];
+ if (!realpath(executablePath, resolvedExecutablePath)) {
+ perror("Could not resolve the application executable path");
+ return 1;
+ }
+
+ char executableDirectoryPath[PATH_MAX];
+ strncpy(executableDirectoryPath, resolvedExecutablePath, sizeof(executableDirectoryPath));
+ executableDirectoryPath[sizeof(executableDirectoryPath) - 1] = '\0';
+ char *executableDirectory = dirname(executableDirectoryPath);
+
+ char scriptPath[PATH_MAX];
+ char launchScriptPath[PATH_MAX];
+ char frameworkPath[PATH_MAX];
+ char luaPath[PATH_MAX * 3];
+ char luaCPath[PATH_MAX * 2];
+ snprintf(scriptPath, sizeof(scriptPath), "%s/../Resources/src", executableDirectory);
+ snprintf(launchScriptPath, sizeof(launchScriptPath), "%s/Launch.lua", scriptPath);
+ snprintf(frameworkPath, sizeof(frameworkPath), "%s/../Frameworks", executableDirectory);
+ snprintf(luaPath, sizeof(luaPath), "%s/../Resources/runtime/lua/?.lua;%s/../Resources/runtime/lua/?/init.lua;%s/?.lua;;",
+ executableDirectory, executableDirectory, scriptPath);
+ snprintf(luaCPath, sizeof(luaCPath), "%s/?.so;;", frameworkPath);
+
+ if (chdir(scriptPath) != 0) {
+ perror("Could not enter the bundled source directory");
+ return 1;
+ }
+ setenv("LUA_PATH", luaPath, 1);
+ setenv("LUA_CPATH", luaCPath, 1);
+
+ const char *homePath = getenv("HOME");
+ if (homePath) {
+ char applicationSupportPath[PATH_MAX];
+ snprintf(applicationSupportPath, sizeof(applicationSupportPath), "%s/Library/Application Support", homePath);
+ setenv("XDG_DATA_HOME", applicationSupportPath, 1);
+ }
+
+ char **luaArgs = calloc((size_t)argc, sizeof(char *));
+ if (!luaArgs) {
+ fprintf(stderr, "Could not allocate launcher arguments\n");
+ return 1;
+ }
+ luaArgs[0] = launchScriptPath;
+ for (int index = 1; index < argc; index++) {
+ luaArgs[index] = argv[index];
+ }
+ int result = RunLuaFileAsWin(argc, luaArgs);
+ free(luaArgs);
+ return result;
+}
diff --git a/macos/package-release.sh b/macos/package-release.sh
new file mode 100755
index 00000000000..09cbe7c5461
--- /dev/null
+++ b/macos/package-release.sh
@@ -0,0 +1,67 @@
+#!/bin/sh
+# cspell:ignore CDPATH lipo otool vtool
+set -eu
+
+if [ "$#" -ne 3 ]; then
+ printf 'usage: %s \n' "$0" >&2
+ exit 2
+fi
+
+script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
+runtime_app=$1
+version=$2
+output_dir=$3
+
+if ! printf '%s\n' "$version" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+-rc\.[1-9][0-9]*$'; then
+ printf 'version must use X.Y.Z-rc.N: %s\n' "$version" >&2
+ exit 2
+fi
+
+mkdir -p "$output_dir"
+output_dir=$(CDPATH= cd -- "$output_dir" && pwd)
+app="$output_dir/Path of Building (macOS RC).app"
+archive_name="PathOfBuilding-macOS-arm64-v$version.zip"
+archive="$output_dir/$archive_name"
+checksum="$archive.sha256"
+validation_log="$output_dir/automated-validation.log"
+extract_dir=$(mktemp -d "${TMPDIR:-/tmp}/pob-macos-release.XXXXXX")
+
+cleanup() {
+ chmod -R u+w "$extract_dir" "$app" 2>/dev/null || true
+ rm -rf "$extract_dir" "$app"
+}
+trap cleanup EXIT
+trap 'exit 1' HUP INT TERM
+
+rm -f "$archive" "$checksum" "$validation_log"
+"$script_dir/build-app.sh" "$runtime_app" "$app" "$version" > "$validation_log" 2>&1
+
+ditto -c -k --keepParent --sequesterRsrc "$app" "$archive"
+(
+ cd "$output_dir"
+ shasum -a 256 "$archive_name" > "$archive_name.sha256"
+ shasum -a 256 -c "$archive_name.sha256"
+) >> "$validation_log" 2>&1
+
+ditto -x -k "$archive" "$extract_dir"
+extracted_app="$extract_dir/Path of Building (macOS RC).app"
+test -x "$extracted_app/Contents/MacOS/Path of Building"
+test -L "$extracted_app/Contents/Resources/src/SimpleGraphic"
+test -L "$extracted_app/Contents/Resources/SimpleGraphic"
+"$script_dir/verify-app.sh" "$extracted_app" >> "$validation_log" 2>&1
+
+(
+ cd "$extracted_app"
+ find Contents/MacOS Contents/Frameworks -type f -print | while IFS= read -r native_file; do
+ if file "$native_file" | grep -q 'Mach-O'; then
+ printf '\n== %s ==\n' "$native_file"
+ file "$native_file"
+ lipo -info "$native_file"
+ otool -L "$native_file"
+ vtool -show-build "$native_file"
+ fi
+ done
+) >> "$validation_log" 2>&1
+
+POB_MACOS_SMOKE=1 "$extracted_app/Contents/MacOS/Path of Building" >> "$validation_log" 2>&1
+printf '%s\n%s\n%s\n' "$archive" "$checksum" "$validation_log"
diff --git a/macos/verify-app.sh b/macos/verify-app.sh
new file mode 100755
index 00000000000..b95c83f90d1
--- /dev/null
+++ b/macos/verify-app.sh
@@ -0,0 +1,81 @@
+#!/bin/sh
+# cspell:ignore codesign otool vtool
+set -eu
+
+app=${1:?"usage: $0 "}
+contents="$app/Contents"
+
+for path in \
+ "$contents/MacOS/Path of Building" \
+ "$contents/Frameworks/libSimpleGraphic.dylib" \
+ "$contents/Frameworks/lcurl.so" \
+ "$contents/Frameworks/lcurl/safe.so" \
+ "$contents/Frameworks/lua-utf8.so" \
+ "$contents/Frameworks/lzip.so" \
+ "$contents/Frameworks/socket/core.so" \
+ "$contents/Resources/manifest.xml" \
+ "$contents/Resources/src/Launch.lua" \
+ "$contents/Resources/src/installed.cfg" \
+ "$contents/Resources/runtime/lua/xml.lua" \
+ "$contents/Resources/runtime/SimpleGraphic/Fonts" \
+ "$contents/Resources/SimpleGraphic/Fonts"; do
+ if [ ! -e "$path" ]; then
+ echo "missing bundle path: $path" >&2
+ exit 1
+ fi
+done
+
+if find "$contents" \( -name "*.exe" -o -name "*.dll" -o -name "first.run" -o -name "Update.exe" -o -name "UpdateCheck.lua" -o -name "UpdateApply.lua" \) -print -quit | grep -q .; then
+ echo "bundle contains Windows or in-app update files" >&2
+ exit 1
+fi
+
+if ! grep -q 'platform="macos-arm64" updateMode="external" updateUrl="https://' "$contents/Resources/manifest.xml"; then
+ echo "bundle manifest does not enable external macOS updates" >&2
+ exit 1
+fi
+if find "$contents" -type f -perm -200 -print -quit | grep -q .; then
+ echo "bundle contains user-writable files" >&2
+ exit 1
+fi
+
+status=0
+file_list=$(mktemp)
+trap 'rm -f "$file_list"' EXIT
+find "$contents/MacOS" "$contents/Frameworks" -type f -print > "$file_list"
+while IFS= read -r file_path; do
+ if file "$file_path" | grep -q "Mach-O"; then
+ if ! file "$file_path" | grep -q "arm64"; then
+ echo "non-arm64 Mach-O file: $file_path" >&2
+ status=1
+ fi
+ minos=$(vtool -show-build "$file_path" | awk '/minos/ { print $2; exit }')
+ if [ -z "$minos" ] || ! awk -v minos="$minos" 'BEGIN { exit !(minos + 0 <= 13) }'; then
+ echo "unexpected deployment target for $file_path: $minos" >&2
+ status=1
+ fi
+ install_name=$(otool -D "$file_path" 2>/dev/null | awk 'NR == 2 { print $1 }')
+ while IFS= read -r dependency; do
+ if [ "$dependency" = "$install_name" ]; then
+ continue
+ fi
+ case "$dependency" in
+ @rpath/*)
+ if [ ! -e "$contents/Frameworks/${dependency#@rpath/}" ]; then
+ echo "unresolved dependency in $file_path: $dependency" >&2
+ status=1
+ fi
+ ;;
+ /System/*|/usr/lib/*) ;;
+ *) echo "non-relocatable dependency in $file_path: $dependency" >&2; status=1 ;;
+ esac
+ done < 1 { print $1 }')
+EOF
+ fi
+done < "$file_list"
+
+if [ "$status" -ne 0 ]; then
+ exit "$status"
+fi
+codesign --verify --deep --strict --verbose=2 "$app"
diff --git a/spec/System/TestBuildListHelpers_spec.lua b/spec/System/TestBuildListHelpers_spec.lua
new file mode 100644
index 00000000000..7256783da30
--- /dev/null
+++ b/spec/System/TestBuildListHelpers_spec.lua
@@ -0,0 +1,42 @@
+-- cspell:ignore imherebuddy
+describe("build list folder scanning", function()
+ local originalNewFileSearch
+ local originalBuildPath
+
+ before_each(function()
+ originalNewFileSearch = _G.NewFileSearch
+ originalBuildPath = main.buildPath
+ main.buildPath = "/builds/"
+ end)
+
+ after_each(function()
+ _G.NewFileSearch = originalNewFileSearch
+ main.buildPath = originalBuildPath
+ end)
+
+ it("keeps folders when the host cannot read their modified time", function()
+ local searchCount = 0
+ _G.NewFileSearch = function()
+ searchCount = searchCount + 1
+ if searchCount == 1 then
+ return nil
+ end
+ return {
+ GetFileName = function() return "imherebuddy" end,
+ GetFileModifiedTime = function()
+ error("filesystem error: in file_size: Is a directory")
+ end,
+ NextFile = function() return false end,
+ }
+ end
+
+ local helpers = LoadModule("Modules/BuildListHelpers")
+ local list = helpers.ScanFolder()
+
+ assert.are.same({ {
+ folderName = "imherebuddy",
+ subPath = "",
+ fullFileName = "/builds/imherebuddy",
+ } }, list)
+ end)
+end)
diff --git a/spec/System/TestUpdatePolicy_spec.lua b/spec/System/TestUpdatePolicy_spec.lua
new file mode 100644
index 00000000000..e600bb0e80d
--- /dev/null
+++ b/spec/System/TestUpdatePolicy_spec.lua
@@ -0,0 +1,81 @@
+describe("update policy", function()
+ it("defaults manifests without a mode to in-app updates", function()
+ assert.are.equal("in-app", launch.updateMode)
+ assert.is_true(launch:SupportsInAppUpdates())
+ end)
+end)
+
+describe("shortcut modifier label", function()
+ local versionPlatform
+
+ before_each(function()
+ versionPlatform = launch.versionPlatform
+ end)
+
+ after_each(function()
+ launch.versionPlatform = versionPlatform
+ end)
+
+ it("uses Cmd for macOS and Ctrl for Windows", function()
+ launch.versionPlatform = "macos-arm64"
+ assert.are.equal("Cmd", launch:GetShortcutModifierLabel())
+
+ launch.versionPlatform = "win32"
+ assert.are.equal("Ctrl", launch:GetShortcutModifierLabel())
+ end)
+end)
+
+describe("external update policy", function()
+ local originalLoadModule
+ local originalLaunchSubScript
+ local originalOpenURL
+ local originalSpawnProcess
+ local originalRestart
+ local originalExit
+
+ before_each(function()
+ originalLoadModule = _G.LoadModule
+ originalLaunchSubScript = _G.LaunchSubScript
+ originalOpenURL = _G.OpenURL
+ originalSpawnProcess = _G.SpawnProcess
+ originalRestart = _G.Restart
+ originalExit = _G.Exit
+ launch.updateMode = "external"
+ launch.updateUrl = "https://github.com/rodgons/PathOfBuilding/releases"
+ end)
+
+ after_each(function()
+ _G.LoadModule = originalLoadModule
+ _G.LaunchSubScript = originalLaunchSubScript
+ _G.OpenURL = originalOpenURL
+ _G.SpawnProcess = originalSpawnProcess
+ _G.Restart = originalRestart
+ _G.Exit = originalExit
+ launch.updateMode = "in-app"
+ launch.updateUrl = nil
+ end)
+
+ it("suppresses checks and application", function()
+ local calls = 0
+ _G.LoadModule = function() calls = calls + 1 end
+ _G.LaunchSubScript = function() calls = calls + 1 end
+ _G.SpawnProcess = function() calls = calls + 1 end
+ _G.Restart = function() calls = calls + 1 end
+ _G.Exit = function() calls = calls + 1 end
+
+ launch:CheckForUpdate()
+ launch:ApplyUpdate("basic")
+
+ assert.are.equal(0, calls)
+ end)
+
+ it("opens the configured release page", function()
+ local openedUrl
+ _G.OpenURL = function(url) openedUrl = url end
+
+ main.controls.checkUpdate.onClick()
+
+ assert.are.equal("https://github.com/rodgons/PathOfBuilding/releases", openedUrl)
+ assert.are.equal("Get macOS RC Updates", main.controls.checkUpdate:GetProperty("label"))
+ end)
+end)
diff --git a/src/Classes/ItemsTab.lua b/src/Classes/ItemsTab.lua
index 7171ea2db24..9ec0dc40bc8 100644
--- a/src/Classes/ItemsTab.lua
+++ b/src/Classes/ItemsTab.lua
@@ -303,8 +303,7 @@ local ItemsTabClass = newClass("ItemsTab", "UndoHandler", "ControlHost", "Contro
self.controls.newDisplayItem = new("ButtonControl", {"TOPLEFT",self.controls.craftDisplayItem,"TOPRIGHT"}, {8, 0, 120, 20}, "Create custom...", function()
self:EditDisplayItemText()
end)
- self.controls.displayItemTip = new("LabelControl", {"TOPLEFT",self.controls.craftDisplayItem,"BOTTOMLEFT"}, {0, 8, 100, 16},
-[[^7Double-click an item from one of the lists,
+ local displayItemTip = [[^7Double-click an item from one of the lists,
or copy and paste an item from in game
(hover over the item and Ctrl+C) to view or edit
the item and add it to your build. You can
@@ -315,7 +314,9 @@ You can Control + Click an item to equip it, or
drag it onto the slot. This will also add it to
your build if it's from the unique/template list.
If there's 2 slots an item can go in,
-holding Shift will put it in the second.]])
+holding Shift will put it in the second.]]
+ displayItemTip = displayItemTip:gsub("Ctrl", launch:GetShortcutModifierLabel()):gsub("Control", launch:GetShortcutModifierLabel())
+ self.controls.displayItemTip = new("LabelControl", {"TOPLEFT",self.controls.craftDisplayItem,"BOTTOMLEFT"}, {0, 8, 100, 16}, displayItemTip)
self.controls.sharedItemList = new("SharedItemListControl", {"TOPLEFT",self.controls.craftDisplayItem, "BOTTOMLEFT"}, {0, 232, 340, 308}, self, true)
-- Display item
@@ -4487,7 +4488,7 @@ function ItemsTabClass:AddItemTooltip(tooltip, item, slot, dbMode, maxWidth)
-- Stat differences
if not self.showStatDifferences then
tooltip:AddSeparator(14)
- tooltip:AddLine(14, colorCodes.TIP.."Tip: Press Ctrl+D to enable the display of stat differences.")
+ tooltip:AddLine(14, colorCodes.TIP.."Tip: Press "..launch:GetShortcutModifierLabel().."+D to enable the display of stat differences.")
return
end
local calcFunc, calcBase = self.build.calcsTab:GetMiscCalculator()
@@ -4745,7 +4746,7 @@ function ItemsTabClass:AddItemTooltip(tooltip, item, slot, dbMode, maxWidth)
end
end
- tooltip:AddLine(14, colorCodes.TIP .. "Tip: Press Ctrl+D to disable the display of stat differences.")
+ tooltip:AddLine(14, colorCodes.TIP .. "Tip: Press "..launch:GetShortcutModifierLabel().."+D to disable the display of stat differences.")
local function getReplacedItemAndOutput(compareSlot)
local selItem = self.items[compareSlot.selItemId]
diff --git a/src/Classes/NotesTab.lua b/src/Classes/NotesTab.lua
index f78ea2eb41a..c6ad9ab8923 100644
--- a/src/Classes/NotesTab.lua
+++ b/src/Classes/NotesTab.lua
@@ -17,6 +17,7 @@ local NotesTabClass = newClass("NotesTab", "ControlHost", "Control", function(se
local notesDesc = [[^7You can use Ctrl +/- (or Ctrl+Scroll) to zoom in and out and Ctrl+0 to reset.
This field also supports different colors. Using the caret symbol (^) followed by a Hex code or a number (0-9) will set the color.
Below are some common color codes PoB uses: ]]
+ notesDesc = notesDesc:gsub("Ctrl", launch:GetShortcutModifierLabel())
self.controls.notesDesc = new("LabelControl", {"TOPLEFT",self,"TOPLEFT"}, {8, 8, 150, 16}, notesDesc)
self.controls.normal = new("ButtonControl", {"TOPLEFT",self.controls.notesDesc,"TOPLEFT"}, {0, 48, 100, 18}, colorCodes.NORMAL.."NORMAL", function() self:SetColor(colorCodes.NORMAL) end)
self.controls.magic = new("ButtonControl", {"TOPLEFT",self.controls.normal,"TOPLEFT"}, {120, 0, 100, 18}, colorCodes.MAGIC.."MAGIC", function() self:SetColor(colorCodes.MAGIC) end)
diff --git a/src/Classes/PassiveTreeView.lua b/src/Classes/PassiveTreeView.lua
index 5d3cb60a524..13cca3aee79 100644
--- a/src/Classes/PassiveTreeView.lua
+++ b/src/Classes/PassiveTreeView.lua
@@ -1464,7 +1464,7 @@ function PassiveTreeViewClass:AddNodeTooltip(tooltip, node, build)
if socket:IsEnabled() then
tooltip:AddLine(14, colorCodes.TIP.."Tip: Right click this socket to go to the items page and choose the jewel for this socket.")
end
- tooltip:AddLine(14, colorCodes.TIP.."Tip: Hold Shift or Ctrl to hide this tooltip.")
+ tooltip:AddLine(14, colorCodes.TIP.."Tip: Hold Shift or "..launch:GetShortcutModifierLabel().." to hide this tooltip.")
return
end
@@ -1472,7 +1472,7 @@ function PassiveTreeViewClass:AddNodeTooltip(tooltip, node, build)
if node.type == "Socket" and not node.alloc then
local socket = build.itemsTab:GetSocketAndJewelForNodeID(node.id)
if addCompareJewelSection(socket, false) then
- tooltip:AddLine(14, colorCodes.TIP.."Tip: Hold Shift or Ctrl to hide this tooltip.")
+ tooltip:AddLine(14, colorCodes.TIP.."Tip: Hold Shift or "..launch:GetShortcutModifierLabel().." to hide this tooltip.")
return
end
end
@@ -1668,10 +1668,10 @@ function PassiveTreeViewClass:AddNodeTooltip(tooltip, node, build)
tooltip:AddLine(14, string.format("^7No changes from %s this node%s.", node.alloc and "unallocating" or "allocating", node.intuitiveLeapLikesAffecting == 0 and pathLength > 1 and " or the nodes leading to it" or ""))
end
end
- tooltip:AddLine(14, colorCodes.TIP.."Tip: Press Ctrl+D to disable the display of stat differences.")
+ tooltip:AddLine(14, colorCodes.TIP.."Tip: Press "..launch:GetShortcutModifierLabel().."+D to disable the display of stat differences.")
else
tooltip:AddSeparator(14)
- tooltip:AddLine(14, colorCodes.TIP.."Tip: Press Ctrl+D to enable the display of stat differences.")
+ tooltip:AddLine(14, colorCodes.TIP.."Tip: Press "..launch:GetShortcutModifierLabel().."+D to enable the display of stat differences.")
end
-- Pathing distance
@@ -1703,10 +1703,10 @@ function PassiveTreeViewClass:AddNodeTooltip(tooltip, node, build)
tooltip:AddLine(14, colorCodes.TIP)
end
if node.type == "Socket" then
- tooltip:AddLine(14, colorCodes.TIP.."Tip: Hold Shift or Ctrl to hide this tooltip.")
+ tooltip:AddLine(14, colorCodes.TIP.."Tip: Hold Shift or "..launch:GetShortcutModifierLabel().." to hide this tooltip.")
else
- tooltip:AddLine(14, colorCodes.TIP.."Tip: Hold Ctrl to hide this tooltip.")
- tooltip:AddLine(14, colorCodes.TIP.."Tip: Press Ctrl+C to copy this node's text.")
+ tooltip:AddLine(14, colorCodes.TIP.."Tip: Hold "..launch:GetShortcutModifierLabel().." to hide this tooltip.")
+ tooltip:AddLine(14, colorCodes.TIP.."Tip: Press "..launch:GetShortcutModifierLabel().."+C to copy this node's text.")
end
end
diff --git a/src/Classes/SkillsTab.lua b/src/Classes/SkillsTab.lua
index 8c4644c701e..9029b1006c6 100644
--- a/src/Classes/SkillsTab.lua
+++ b/src/Classes/SkillsTab.lua
@@ -107,15 +107,14 @@ local SkillsTabClass = newClass("SkillsTab", "UndoHandler", "ControlHost", "Cont
-- Socket group list
self.controls.groupList = new("SkillListControl", { "TOPLEFT", self, "TOPLEFT" }, { 20, 54, 360, 300 }, self)
- self.controls.groupTip = new("LabelControl", { "TOPLEFT", self.controls.groupList, "BOTTOMLEFT" }, { 0, 8, 0, 14 },
-[[
+ local groupTip = [[
^7Usage Tips:
- You can copy/paste socket groups using Ctrl+C and Ctrl+V.
- Ctrl + Click to enable/disable socket groups.
- Ctrl + Right click to include/exclude in FullDPS calculations.
- Right click to set as the Main skill group.
]]
- )
+ self.controls.groupTip = new("LabelControl", { "TOPLEFT", self.controls.groupList, "BOTTOMLEFT" }, { 0, 8, 0, 14 }, groupTip:gsub("Ctrl", launch:GetShortcutModifierLabel()))
-- Gem options
local optionInputsX = 170
diff --git a/src/Classes/TradeQuery.lua b/src/Classes/TradeQuery.lua
index ad6b6a48f39..a50a81a39f8 100644
--- a/src/Classes/TradeQuery.lua
+++ b/src/Classes/TradeQuery.lua
@@ -1109,7 +1109,7 @@ you can add them, copy the link here, and press "Price Item" to evaluate the ite
controls["uri"..row_idx].tooltipFunc = function(tooltip)
tooltip:Clear()
if controls["uri" .. row_idx].buf:find('^' .. self.hostNamePattern .. 'trade/search/') ~= nil then
- tooltip:AddLine(16, "Control + click to open in web-browser")
+ tooltip:AddLine(16, launch:GetShortcutModifierLabel().." + click to open in web-browser")
end
end
controls["priceButton"..row_idx] = new("ButtonControl", { "TOPLEFT", controls["uri"..row_idx], "TOPRIGHT"}, {8, 0, 100, row_height}, "Price Item",
diff --git a/src/Launch.lua b/src/Launch.lua
index 2453884dafe..95577feddc5 100644
--- a/src/Launch.lua
+++ b/src/Launch.lua
@@ -17,31 +17,41 @@ SetMainObject(launch)
jit.opt.start('maxtrace=4000','maxmcode=8192')
collectgarbage("setpause", 400)
+function launch:SupportsInAppUpdates()
+ return self.updateMode == "in-app"
+end
+
+function launch:UsesExternalUpdates()
+ return self.updateMode == "external"
+end
+
+function launch:HasExternalUpdatePage()
+ return self:UsesExternalUpdates() and self.updateUrl and self.updateUrl:match("^https://") ~= nil
+end
+
+function launch:OpenExternalUpdatePage()
+ if not self:HasExternalUpdatePage() then
+ return false
+ end
+ OpenURL(self.updateUrl)
+ return true
+end
+
+function launch:GetShortcutModifierLabel()
+ return self.versionPlatform == "macos-arm64" and "Cmd" or "Ctrl"
+end
+
function launch:OnInit()
self.devMode = false
self.installedMode = false
self.versionNumber = "?"
self.versionBranch = "?"
self.versionPlatform = "?"
+ self.updateMode = "in-app"
+ self.updateUrl = nil
self.lastUpdateCheck = GetTime()
self.subScripts = { }
self.startTime = startTime
- local firstRunFile = io.open("first.run", "r")
- if firstRunFile then
- firstRunFile:close()
- os.remove("first.run")
- -- This is a fresh installation
- -- Perform an immediate update to download the latest version
- ConClear()
- ConPrintf("Please wait while we complete installation...\n")
- local updateMode, errMsg = LoadModule("UpdateCheck")
- if not updateMode then
- self.updateErrMsg = errMsg
- elseif updateMode ~= "none" then
- self:ApplyUpdate(updateMode)
- return
- end
- end
local xml = require("xml")
local localManXML = xml.LoadXMLFile("manifest.xml") or xml.LoadXMLFile("../manifest.xml")
if localManXML and localManXML[1].elem == "PoBVersion" then
@@ -51,10 +61,15 @@ function launch:OnInit()
self.versionNumber = node.attrib.number
self.versionBranch = node.attrib.branch
self.versionPlatform = node.attrib.platform
+ self.updateMode = node.attrib.updateMode or "in-app"
+ self.updateUrl = node.attrib.updateUrl
end
end
end
end
+ if self:UsesExternalUpdates() and not self:HasExternalUpdatePage() then
+ self.updatePolicyErrMsg = "External update policy requires an HTTPS update URL."
+ end
if localManXML and not self.versionBranch and not self.versionPlatform then
-- Looks like a remote manifest, so we're probably running from a repository
-- Enable dev mode to disable updates and set user path to be the script path
@@ -65,6 +80,24 @@ function launch:OnInit()
self.installedMode = true
installedFile:close()
end
+ local firstRunFile = io.open("first.run", "r")
+ if firstRunFile then
+ firstRunFile:close()
+ if self:SupportsInAppUpdates() then
+ os.remove("first.run")
+ -- This is a fresh installation
+ -- Perform an immediate update to download the latest version
+ ConClear()
+ ConPrintf("Please wait while we complete installation...\n")
+ local updateMode, errMsg = LoadModule("UpdateCheck")
+ if not updateMode then
+ self.updateErrMsg = errMsg
+ elseif updateMode ~= "none" then
+ self:ApplyUpdate(updateMode)
+ return
+ end
+ end
+ end
RenderInit("DPI_AWARE")
ConPrintf("Loading main script...")
local errMsg
@@ -79,8 +112,16 @@ function launch:OnInit()
self:ShowErrMsg("In 'Init': %s", errMsg)
end
end
+ if self.updatePolicyErrMsg then
+ self:ShowErrMsg(self.updatePolicyErrMsg)
+ end
+ if os.getenv("POB_MACOS_SMOKE") == "1" and not self.promptMsg then
+ ConPrintf("POB MACOS SMOKE: application initialization passed.\n")
+ Exit()
+ return
+ end
- if not self.devMode and not firstRunFile then
+ if self:SupportsInAppUpdates() and not self.devMode and not firstRunFile then
-- Run a background update check if developer mode is off
self:CheckForUpdate(true)
end
@@ -133,7 +174,7 @@ function launch:OnFrame()
DrawString(0, screenH/2, "CENTER", 24, "FIXED", self.doRestart)
Restart()
end
- if not self.devMode and (GetTime() - self.lastUpdateCheck) > 1000*60*60*12 then
+ if self:SupportsInAppUpdates() and not self.devMode and (GetTime() - self.lastUpdateCheck) > 1000*60*60*12 then
-- Do an update check every 12 hours if the user keeps the program open
self:CheckForUpdate(true)
end
@@ -156,7 +197,7 @@ function launch:OnKeyDown(key, doubleClick)
profiling = true
end
elseif key == "u" and IsKeyDown("CTRL") then
- if not self.devMode then
+ if self:SupportsInAppUpdates() and not self.devMode then
self:CheckForUpdate()
end
elseif key == "PRINTSCREEN" and IsKeyDown("CTRL") then
@@ -323,6 +364,9 @@ function launch:DownloadPage(url, callback, params)
end
function launch:ApplyUpdate(mode)
+ if not self:SupportsInAppUpdates() then
+ return false
+ end
if mode == "basic" then
-- Need to revert to the basic environment to fully apply the update
LoadModule("UpdateApply", "Update/opFile.txt")
@@ -334,11 +378,12 @@ function launch:ApplyUpdate(mode)
Restart()
self.doRestart = "Updating..."
end
+ return true
end
function launch:CheckForUpdate(inBackground)
- if self.updateCheckRunning then
- return
+ if not self:SupportsInAppUpdates() or self.updateCheckRunning then
+ return false
end
self.updateCheckBackground = inBackground
self.updateMsg = "Initialising..."
@@ -353,6 +398,7 @@ function launch:CheckForUpdate(inBackground)
self.updateCheckRunning = true
end
update:close()
+ return true
end
function launch:ShowPrompt(r, g, b, str, func)
@@ -383,7 +429,7 @@ function launch:ShowErrMsg(fmt, ...)
local version = self.versionNumber and
"^8v"..self.versionNumber..(self.versionBranch and " "..self.versionBranch or "")
or ""
- self:ShowPrompt(1, 0, 0, "^1Error:\n\n^0"..string.format(fmt, ...).."\n"..version.."\n^0Press Enter/Escape to dismiss, F4 to return to build selection, or F5 to restart the application.\nPress CTRL + C to copy error text.")
+ self:ShowPrompt(1, 0, 0, "^1Error:\n\n^0"..string.format(fmt, ...).."\n"..version.."\n^0Press Enter/Escape to dismiss, F4 to return to build selection, or F5 to restart the application.\nPress "..self:GetShortcutModifierLabel().." + C to copy error text.")
end
end
diff --git a/src/Modules/Build.lua b/src/Modules/Build.lua
index b809dbe01df..5540760effb 100644
--- a/src/Modules/Build.lua
+++ b/src/Modules/Build.lua
@@ -1364,7 +1364,7 @@ function buildMode:OpenSavePopup(mode)
self:CloseBuild()
elseif mode == "EXIT" then
Exit()
- elseif mode == "UPDATE" then
+ elseif mode == "UPDATE" and launch:SupportsInAppUpdates() then
launch:ApplyUpdate(launch.updateAvailable)
end
end)
@@ -2079,7 +2079,7 @@ function buildMode:SaveDBFile()
self:CloseBuild()
elseif action == "EXIT" then
Exit()
- elseif action == "UPDATE" then
+ elseif action == "UPDATE" and launch:SupportsInAppUpdates() then
launch:ApplyUpdate(launch.updateAvailable)
end
end
diff --git a/src/Modules/BuildListHelpers.lua b/src/Modules/BuildListHelpers.lua
index aefc34f1745..27f765264d9 100644
--- a/src/Modules/BuildListHelpers.lua
+++ b/src/Modules/BuildListHelpers.lua
@@ -63,11 +63,12 @@ local function ScanFolder(subPath, filterText)
handle = NewFileSearch(main.buildPath..subPath.."*", true)
while handle do
local folderName = handle:GetFileName()
+ local modifiedOk, modified = pcall(handle.GetFileModifiedTime, handle)
t_insert(list, {
folderName = folderName,
subPath = subPath,
fullFileName = main.buildPath..subPath..folderName,
- modified = handle:GetFileModifiedTime()
+ modified = modifiedOk and modified or nil
})
if not handle:NextFile() then
break
diff --git a/src/Modules/Main.lua b/src/Modules/Main.lua
index 0e7b0d48c1b..5e6132cf85a 100644
--- a/src/Modules/Main.lua
+++ b/src/Modules/Main.lua
@@ -213,15 +213,25 @@ function main:Init()
self:OpenUpdatePopup()
end)
self.controls.applyUpdate.shown = function()
- return launch.updateAvailable and launch.updateAvailable ~= "none"
+ return launch:SupportsInAppUpdates() and launch.updateAvailable and launch.updateAvailable ~= "none"
end
self.controls.checkUpdate = new("ButtonControl", {"BOTTOMLEFT",self.anchorMain,"BOTTOMLEFT"}, {0, -24, 140, 20}, "", function()
- launch:CheckForUpdate()
+ if launch:UsesExternalUpdates() then
+ launch:OpenExternalUpdatePage()
+ else
+ launch:CheckForUpdate()
+ end
end)
self.controls.checkUpdate.shown = function()
- return not launch.devMode and (not launch.updateAvailable or launch.updateAvailable == "none")
+ if launch:UsesExternalUpdates() then
+ return launch:HasExternalUpdatePage()
+ end
+ return launch:SupportsInAppUpdates() and not launch.devMode and (not launch.updateAvailable or launch.updateAvailable == "none")
end
self.controls.checkUpdate.label = function()
+ if launch:UsesExternalUpdates() then
+ return "Get macOS RC Updates"
+ end
return launch.updateCheckRunning and launch.updateProgress or "Check for Update"
end
self.controls.checkUpdate.enabled = function()
@@ -233,6 +243,9 @@ function main:Init()
end
self.controls.versionLabel = new("LabelControl", {"BOTTOMLEFT",self.anchorMain,"BOTTOMLEFT"}, {148, -2, 0, 16}, "")
self.controls.versionLabel.label = function()
+ if launch.versionPlatform == "macos-arm64" then
+ return "^8macOS RC: " .. launch.versionNumber
+ end
return "^8" .. (launch.versionBranch == "beta" and "Beta: " or "Version: ") .. launch.versionNumber .. (launch.versionBranch == "dev" and " (Dev)" or "")
end
self.controls.devMode = new("LabelControl", {"BOTTOMLEFT",self.anchorMain,"BOTTOMLEFT"}, {0, -26, 0, 20}, colorCodes.NEGATIVE.."Dev Mode")
@@ -371,11 +384,11 @@ function main:OnFrame()
self:CallMode("OnFrame", self.inputEvents, self.viewPort)
- if launch.updateErrMsg then
+ if launch:SupportsInAppUpdates() and launch.updateErrMsg then
ToastNotification:Add(string.format("Update check failed!\n%s", launch.updateErrMsg))
launch.updateErrMsg = nil
end
- if launch.updateAvailable then
+ if launch:SupportsInAppUpdates() and launch.updateAvailable then
if launch.updateAvailable == "none" then
ToastNotification:Add("No update available\nYou are running the latest version.")
launch.updateAvailable = nil
@@ -1006,6 +1019,9 @@ function main:OpenOptionsPopup(savedState)
controls.betaTest = new("CheckBoxControl", { "TOPLEFT", controls.sectionAnchor, "TOPLEFT" }, { currentX + defaultLabelPlacementX, currentY, 20 }, "^7Opt-in to weekly beta test builds:", function(state)
self.betaTest = state
end)
+ controls.betaTest.shown = function()
+ return launch:SupportsInAppUpdates()
+ end
nextRow()
controls.edgeSearchHighlight = new("CheckBoxControl", { "TOPLEFT", controls.sectionAnchor, "TOPLEFT" }, { currentX + defaultLabelPlacementX, currentY, 20}, "^7Show search circles at viewport edge", function(state)
@@ -1181,7 +1197,7 @@ function main:OpenOptionsPopup(savedState)
if self.mode == "LIST" then
self.modes.LIST:BuildList()
end
- if not launch.devMode then
+ if launch:SupportsInAppUpdates() and not launch.devMode then
main:SetManifestBranch(self.betaTest and "beta" or "master")
end
SetDPIScaleOverridePercent(self.dpiScaleOverridePercent)
@@ -1278,6 +1294,9 @@ function main:OpenOptionsPopup(savedState)
end
function main:SetManifestBranch(branchName)
+ if not launch:SupportsInAppUpdates() then
+ return false
+ end
local xml = require("xml")
local manifestLocation = "manifest.xml"
local localManXML = xml.LoadXMLFile(manifestLocation)
@@ -1295,9 +1314,13 @@ function main:SetManifestBranch(branchName)
end
end
xml.SaveXMLFile(localManXML[1], manifestLocation)
+ return true
end
function main:OpenUpdatePopup()
+ if not launch:SupportsInAppUpdates() then
+ return
+ end
local changeList = { }
local changelogName = launch.devMode and "../changelog.txt" or "changelog.txt"
local changelogFile = io.open(changelogName, "r")
@@ -1336,6 +1359,9 @@ end
function main:OpenAboutPopup(helpSectionIndex)
local textSize, subTitleSize, titleSize, popupWidth = 16, 20, 24, 810
local changeList = { }
+ if launch:UsesExternalUpdates() then
+ t_insert(changeList, { height = textSize * 3, "^7This experimental macOS RC does not update itself. Download a newer .app from the fork prerelease page and replace this copy manually." })
+ end
local changeVersionHeights = { }
local changelogName = launch.devMode and "../changelog.txt" or "changelog.txt"
local changelogFile = io.open(changelogName, "r")