Preserve installed launchers during setup and show clear setup stages

This commit is contained in:
Erick Sanchez committed 2026-10-26 14:16:47 -06:00
1 parent a5ed1e8068
commit d151d54fd1
6 files changed
+88 -9

No files matched your search

+1 -1
View File
@@ -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?
+2 -2
View File
@@ -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?
+2 -2
View File
@@ -1,5 +1,5 @@
{
"version": "1.4",
"build": 5,
"version": "1.4.1",
"build": 6,
"minimumMacOSMajor": 26
}
+4 -2
View File
@@ -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
+29 -2
View File
@@ -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()
+50
View File
@@ -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')