diff --git a/README-es.md b/README-es.md index 4769d04..17b6d7c 100644 --- a/README-es.md +++ b/README-es.md @@ -58,7 +58,7 @@ La actualización usa una carpeta separada y guarda la app anterior como respald ![Aviso de actualización dentro de la app](docs/images/nxnative-update-es-v1.4.png) -**La app vieja de tu amigo todavía no tiene actualizador:** que ejecute una vez el comando de arriba o abra **START-HERE.command** para recibir esta versión. Las siguientes actualizaciones ya aparecerán dentro de la app. +**¿Tienes la versión 1.3 o anterior?** Ejecuta una vez el comando de arriba o abre **START-HERE.command**. Las siguientes actualizaciones ya aparecerán dentro de la app. ## ¿Qué pasa al pulsar Preparar juego? diff --git a/README.md b/README.md index ee850ec..fd06b80 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/nalancer08/nxnative-easy/main/install.sh)" ``` -It downloads nxnative Easy, installs Homebrew and the missing build tools, **compiles your own Mac interface and opens it**. Follow any password or confirmation prompts in Terminal. No Apple developer account is used. +It downloads nxnative Easy, installs Homebrew and the missing build tools, **compiles your own Mac interface and opens it**. Follow the four numbered setup stages and any password or confirmation prompts in Terminal. No Apple developer account is used. If you run setup again, it verifies the new app before replacing your installed version and keeps the previous app as a backup. **Already downloaded this repository?** Double-click **START-HERE.command** in its top-level folder. It performs the same setup; you do not need to find a script under `scripts/`. If macOS blocks that file, use the Terminal command above. @@ -56,7 +56,7 @@ From **version 1.4**, the app checks for new releases at launch and every six ho The update uses a separate source folder and keeps the previous app as a backup. It does not reset your checkout or modify your game files, keys or saves. Updates are disabled while preparing a game. To apply new engine or profile changes to a previously prepared game, prepare that game again. -**Your friend's older app has no updater yet:** run the setup command above once, or double-click **START-HERE.command**, to get this version. Future updates then appear inside the app. +**Using version 1.3 or earlier?** Run the setup command above once, or double-click **START-HERE.command**. Future updates then appear inside the app. ## What happens after Prepare game? diff --git a/desktop/release.json b/desktop/release.json index 59a97fc..31c6b80 100644 --- a/desktop/release.json +++ b/desktop/release.json @@ -1,5 +1,5 @@ { - "version": "1.4", - "build": 5, + "version": "1.4.1", + "build": 6, "minimumMacOSMajor": 26 } diff --git a/install.sh b/install.sh index 141d194..86017f3 100755 --- a/install.sh +++ b/install.sh @@ -7,6 +7,7 @@ case "${1:-}" in --check) nxe_check=1;; '') ;; *) echo 'Usage: bash install.sh [ [ "$(uname -m)" = arm64 ] || { echo 'An Apple silicon Mac (M1 or newer) is required.' >&2; exit 1; } nxe_os=$(/usr/bin/sw_vers -productVersion) [ "${nxe_os%%.*}" -ge 26 ] || { echo 'Update this Mac to macOS 26 or newer first.' >&2; exit 1; } +echo "[1/4] Checking your Mac and Xcode / Revisando tu Mac y Xcode…" nxe_developer=${DEVELOPER_DIR:-$(/usr/bin/xcode-select -p 2>/dev/null || true)} if [ ! -d "$nxe_developer/Platforms/MacOSX.platform" ]; then nxe_developer=/Applications/Xcode.app/Contents/Developer; fi if [ ! -d "$nxe_developer/Platforms/MacOSX.platform" ]; then @@ -27,6 +28,7 @@ if [ "$nxe_check" -eq 1 ]; then if [ -n "$nxe_brew" ]; then echo 'Homebrew: ready.'; else echo 'Homebrew: will be installed by setup.'; fi exit 0 fi +echo "[2/4] Preparing Homebrew and source / Preparando Homebrew y código…" if [ -z "$nxe_brew" ]; then echo 'Installing Homebrew / Instalando Homebrew. It may request your Mac password.' /bin/bash -c "$(/usr/bin/curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" @@ -54,9 +56,9 @@ elif [ -d "$nxe_source/.git" ]; then else echo 'The setup folder already contains other files; preserved without changes.' >&2; exit 1 fi -echo 'Preparing tools / Preparando herramientas…' +echo '[3/4] Preparing tools / Preparando herramientas…' /bin/bash "$nxe_source/scripts/prepare_ios.sh" --target mac --setup-only "$nxe_tools" -echo 'Building your Mac app / Compilando tu app de Mac…' +echo '[4/4] Building and verifying your Mac app / Compilando y verificando tu app…' "$nxe_tools/nxnative-tools/venv/bin/python" "$nxe_source/scripts/build_desktop_app.py" --output "$nxe_app" echo "Ready / Lista: $nxe_app" if [ "${NXNATIVE_NO_OPEN:-0}" != 1 ]; then /usr/bin/open "$nxe_app"; fi diff --git a/scripts/build_desktop_app.py b/scripts/build_desktop_app.py index b9b632f..57f2dae 100755 --- a/scripts/build_desktop_app.py +++ b/scripts/build_desktop_app.py @@ -8,6 +8,8 @@ import sys import tempfile import shutil import json +import os +import uuid REPO=Path(__file__).resolve().parents[1] @@ -19,6 +21,33 @@ def main(): output=args.output.expanduser().absolute() if output==REPO or REPO in output.parents:parser.error('Keep build outputs outside the repository.') if output.suffix!='.app':parser.error('Output must end with .app') + output.parent.mkdir(parents=True,exist_ok=True) + # Compile beside the destination. A compiler/signing failure must preserve the installed app. + with tempfile.TemporaryDirectory(prefix='.nxnative-build-',dir=output.parent) as temporary: + staged=Path(temporary)/output.name + build_bundle(staged) + subprocess.run(['codesign','--verify','--deep','--strict',str(staged)],check=True) + install_bundle(staged,output) + if args.open:subprocess.run(['open',str(output)],check=True) + print(output) + +def install_bundle(staged,output): + if not output.exists(): + os.replace(staged,output) + return + info=plistlib.loads((output/'Contents/Info.plist').read_bytes()) + if info.get('CFBundleIdentifier')!='com.nalancer08.nxnative.easy': + raise ValueError('The destination contains another app; it was preserved.') + backup=output.with_name('.'+output.name+'.previous-'+uuid.uuid4().hex[:10]) + os.replace(output,backup) + try: + os.replace(staged,output) + except BaseException: + os.replace(backup,output) + raise + + +def build_bundle(output): contents=output/'Contents';macos=contents/'MacOS';macos.mkdir(parents=True,exist_ok=True) subprocess.run(['xcrun','swiftc','-parse-as-library','-swift-version','5', str(REPO/'desktop/NxNativeEasy/main.swift'),str(REPO/'desktop/NxNativeEasy/Localization.swift'),str(REPO/'desktop/NxNativeEasy/GameCatalog.swift'),str(REPO/'desktop/NxNativeEasy/ControllerMonitor.swift'),str(REPO/'desktop/NxNativeEasy/ControllerTest.swift'),str(REPO/'desktop/NxNativeEasy/UpdateRelease.swift'),str(REPO/'desktop/NxNativeEasy/AppUpdater.swift'),'-o',str(macos/'NxNativeEasy'), @@ -40,7 +69,5 @@ def main(): (contents/'Info.plist').write_bytes(plistlib.dumps(info)) # Explicit ad-hoc signing: '-' never selects an Apple certificate or team. subprocess.run(['codesign','--force','--sign','-','--timestamp=none',str(output)],check=True) - if args.open:subprocess.run(['open',str(output)],check=True) - print(output) if __name__=='__main__':main() diff --git a/scripts/tests/test_desktop_install.py b/scripts/tests/test_desktop_install.py new file mode 100644 index 0000000..5c83556 --- /dev/null +++ b/scripts/tests/test_desktop_install.py @@ -0,0 +1,50 @@ +"""A failed launcher rebuild must not damage a previous installation.""" +import importlib.util +from pathlib import Path +import plistlib +import subprocess +import sys +import tempfile +import unittest +from unittest.mock import patch + +ROOT=Path(__file__).resolve().parents[2] +spec=importlib.util.spec_from_file_location('desktop_builder',ROOT/'scripts/build_desktop_app.py') +builder=importlib.util.module_from_spec(spec);spec.loader.exec_module(builder) + +class DesktopInstall(unittest.TestCase): + def old_app(self,root,identifier='com.nalancer08.nxnative.easy'): + app=root/'Easy.app';(app/'Contents').mkdir(parents=True) + (app/'Contents/Info.plist').write_bytes(plistlib.dumps({'CFBundleIdentifier':identifier})) + (app/'marker').write_text('previous') + return app + + def test_failed_compile_preserves_installed_app(self): + with tempfile.TemporaryDirectory() as temporary: + root=Path(temporary);app=self.old_app(root) + with patch.object(sys,'argv',['builder','--output',str(app)]),patch.object(builder,'build_bundle',side_effect=subprocess.CalledProcessError(1,'swiftc')): + with self.assertRaises(subprocess.CalledProcessError):builder.main() + self.assertEqual((app/'marker').read_text(),'previous') + self.assertEqual(list(root.glob('.nxnative-build-*')),[]) + + def test_success_keeps_backup_and_failed_swap_rolls_back(self): + for fail in (False,True): + with self.subTest(fail=fail),tempfile.TemporaryDirectory() as temporary: + root=Path(temporary);app=self.old_app(root);staged=root/'New.app';staged.mkdir() + (staged/'marker').write_text('new') + replace=builder.os.replace + def swap(source,destination): + if fail and source==staged:raise OSError('simulated swap failure') + return replace(source,destination) + with patch.object(builder.os,'replace',side_effect=swap): + if fail: + with self.assertRaises(OSError):builder.install_bundle(staged,app) + else:builder.install_bundle(staged,app) + self.assertEqual((app/'marker').read_text(),'previous' if fail else 'new') + if not fail:self.assertEqual((next(root.glob('.Easy.app.previous-*'))/'marker').read_text(),'previous') + + def test_other_app_is_preserved(self): + with tempfile.TemporaryDirectory() as temporary: + root=Path(temporary);app=self.old_app(root,'another.app');staged=root/'New.app';staged.mkdir() + with self.assertRaises(ValueError):builder.install_bundle(staged,app) + self.assertEqual((app/'marker').read_text(),'previous')