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")