diff --git a/.gitignore b/.gitignore
index 23d49ea..6eef001 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,3 +29,5 @@ exefs/
save/
Payload/
nxnative-ios.json
+
+nxnative-mac.json
diff --git a/README-es.md b/README-es.md
index 27c47ca..2ea0dac 100644
--- a/README-es.md
+++ b/README-es.md
@@ -2,7 +2,35 @@
[English](README.md) · **Español**
-**Prepara tus juegos para iPhone desde una app de Mac. Sin comandos para el uso diario.**
+**Tus propios juegos, preparados para iPhone, iPad o Mac.**
+
+## Empieza aquí
+
+**Primero:** instala [Xcode](https://apps.apple.com/app/xcode/id497799835) completo desde App Store y ábrelo una vez. **Xcode también compila los juegos**; una interfaz ya compilada no elimina ese requisito.
+
+**Después:** pega este único comando en Terminal:
+
+```bash
+/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/nalancer08/nxnative-easy/main/install.sh)"
+```
+
+Descarga nxnative Easy, instala Homebrew y las herramientas que falten, **compila tu interfaz de Mac y la abre**. Sigue las solicitudes de contraseña o confirmación que aparezcan en Terminal. No usa una cuenta de desarrollador Apple.
+
+**¿Ya descargaste el repositorio?** Abre **START-HERE.command** en la carpeta principal con doble clic. Hace la misma preparación; no necesitas buscar scripts dentro de `scripts/`. Si macOS bloquea ese archivo, usa el comando de arriba.
+
+Cuando abra la app:
+
+1. Elige **iPhone / iPad** o **Mac**.
+2. Selecciona la **carpeta de tu juego** y la **carpeta de tus llaves**.
+3. Pulsa **Preparar juego**.
+
+**Las siguientes veces:** abre **nxnative Easy.app** en la carpeta **Aplicaciones de tu usuario**. La preparación conserva su propia copia del código; puedes borrar el ZIP y la carpeta que descargaste.
+
+**Mac:** crea una `.app` del juego; pulsa **Abrir juego**. No necesitas Sideloadly ni un iPhone. Las partidas quedan en `~/Library/Application Support/nxnative/
/`.
+
+**iPhone / iPad:** crea una IPA ligera sin firmar. Instálala con Sideloadly y copia los datos del juego como te indica la app.
+
+**Necesitas:** Mac con Apple silicon (M1 o posterior), macOS 26+ y Xcode 26+. El comando instala las otras herramientas. Los juegos y las llaves los aportas tú. La primera compilación puede tardar.

@@ -16,22 +44,17 @@ Partimos de [seedds/nxnative](https://github.com/seedds/nxnative): el motor, los
- **Llaves del usuario:** debes seleccionar la carpeta de tus propias `prod.keys`. No incluimos, descargamos ni distribuimos llaves o juegos.
- **Preparación local:** sin cuenta Apple, sin certificados de empresa y sin servicio que reciba tus archivos.
-Entregamos **código, una interfaz de preparación y herramientas de compilación**. El resultado es una IPA sin firmar; tú la firmas e instalas con Sideloadly. No ofrecemos juegos, llaves, firma de Apple ni compatibilidad garantizada con cualquier juego.
+Entregamos **código, una interfaz de preparación y herramientas de compilación**. El resultado es una app local para Mac o una IPA para iPhone/iPad sin firmar, que firmas e instalas con Sideloadly. No ofrecemos juegos, llaves, firma de Apple ni compatibilidad garantizada con cualquier juego.
## Idioma
La app inicia en **English**. Cambia a **Español** con el selector de la esquina superior; la elección se guarda en este Mac. Cambia el idioma de la interfaz, no el del juego. Los detalles técnicos se muestran en su idioma original.
-## Abre la app y elige tu juego
+## ¿Qué pasa al pulsar Preparar juego?
-1. Abre **nxnative.app**.
-2. Elige la **Carpeta de tu juego**, selecciona tu **Carpeta de llaves** una vez y pulsa **Preparar juego**.
+La app prepara las herramientas y compila para el dispositivo que elegiste. **Mac** genera una app del juego con los botones **Abrir juego** y **Mostrar app**. **iPhone / iPad** genera una IPA ligera y muestra los pasos de Sideloadly y Finder al terminar. Tus archivos y salidas anteriores se conservan en la carpeta del juego.
-La app prepara las herramientas, compila y deja lista la **IPA ligera**. Verás el avance en la ventana. Al terminar aparecen los dos pasos para instalar. **Enviar a Sideloadly** abre allí la IPA preparada; si Sideloadly no está instalado, el botón ofrece descargarlo.
-
-**Una vez creada:** abre `nxnative.app`. Recuerda tu última carpeta y permite volver a mostrar la IPA ya preparada.
-
-**En otro Mac:** después de descargar este repositorio, abre [`scripts/Abrir-interfaz.command`](scripts/Abrir-interfaz.command) con doble clic. Crea y abre la app local. La primera vez necesitas Xcode completo y Homebrew instalados; si hacen falta tus llaves, te pedirá elegir la carpeta donde guardas tu propio `prod.keys`. Mantén cada juego fuera del repositorio, en una carpeta con su base y actualizaciones.
+La preparación conserva una copia separada del código y respeta los cambios locales. Para desarrollo sigue disponible [`scripts/Abrir-interfaz.command`](scripts/Abrir-interfaz.command).
## Cómo se compila la app de Mac
diff --git a/README.md b/README.md
index 6830de8..4082723 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,35 @@
**English** · [Español](README-es.md)
-**Prepare your games for iPhone with a native Mac app. No commands for everyday use.**
+**Your own games, prepared for iPhone, iPad or Mac.**
+
+## Start here
+
+**First:** install full [Xcode](https://apps.apple.com/app/xcode/id497799835) from the Mac App Store and open it once. **Xcode compiles the games too**, so a prebuilt launcher cannot remove this requirement.
+
+**Then:** paste this single command into Terminal:
+
+```bash
+/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.
+
+**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.
+
+When the app opens:
+
+1. Choose **iPhone / iPad** or **Mac**.
+2. Choose your own **game folder** and **keys folder**.
+3. Click **Prepare game**.
+
+**After setup:** open **nxnative Easy.app** in your home **Applications** folder. The setup keeps its source checkout separately, so you can delete the downloaded ZIP and folder.
+
+**Mac:** creates a game `.app`; click **Open game**. No Sideloadly or iPhone needed. Saves stay in `~/Library/Application Support/nxnative//`.
+
+**iPhone / iPad:** creates an unsigned lightweight IPA; install it with Sideloadly, then copy the game data as shown in the app.
+
+**Required:** an Apple silicon Mac (M1 or newer), macOS 26+ and Xcode 26+. The command installs the other tools. You must provide your own games and keys. Compilation can take time, especially for the first game.

@@ -16,22 +44,17 @@ Built on [seedds/nxnative](https://github.com/seedds/nxnative). The original pro
- **Your own keys:** explicitly select the external folder containing your `prod.keys`. No games or keys are included, downloaded or distributed.
- **Local preparation:** no Apple account or company certificate is used to build the desktop app.
-We provide **source code, a preparation interface and build tools**. The output is an unsigned IPA that you sign and install yourself with Sideloadly. Games, console keys, Apple signing and universal game compatibility are not provided.
+We provide **source code, a preparation interface and build tools**. The output is a local Mac game app, or an unsigned iPhone/iPad IPA that you sign and install yourself with Sideloadly. Games, console keys, Apple signing and universal game compatibility are not provided.
## Language
The app starts in **English**. Choose **Español** using the selector at the top; your selection is remembered on this Mac. This changes the interface language, not the game's language. Build diagnostics keep their original language.
-## Open the app and choose your game
+## What happens after Prepare game?
-1. Open **nxnative.app**.
-2. Choose **Your game folder**, select your **Keys folder** once, then click **Prepare game**.
+The app prepares the tools and builds for your chosen device. **Mac** produces a game app with **Open game** and **Show app** buttons. **iPhone / iPad** produces a lightweight IPA and shows the Sideloadly and Finder steps when ready. Your own files and previous outputs stay in the game folder.
-The app prepares the tools, builds the game app and creates a **lightweight IPA**. Progress stays in the window. When finished, the app shows the two installation steps. **Send to Sideloadly** opens the prepared IPA in Sideloadly; if it is not installed, the button offers its download.
-
-**First-time setup:** download or clone this repository, then double-click the local [`scripts/Abrir-interfaz.command`](scripts/Abrir-interfaz.command) file in Finder. It creates and opens the Mac app. Full Xcode and Homebrew must already be installed. Select the folder containing your own `prod.keys` when asked. Keep one game per folder, with the base game and its updates, outside this source checkout.
-
-**Already set up:** open the existing `nxnative.app`. It remembers your last game folder and can reveal a previously prepared IPA.
+Setup keeps a separate source checkout and preserves local edits. Developers can still use [`scripts/Abrir-interfaz.command`](scripts/Abrir-interfaz.command).
## How the Mac app is built
diff --git a/START-HERE.command b/START-HERE.command
new file mode 100755
index 0000000..a21cccc
--- /dev/null
+++ b/START-HERE.command
@@ -0,0 +1,4 @@
+#!/bin/bash
+set -euo pipefail
+nxe_here=$(cd "$(dirname "$0")" && pwd -P)
+/bin/bash "$nxe_here/install.sh"
diff --git a/START-HERE.txt b/START-HERE.txt
new file mode 100644
index 0000000..f917572
--- /dev/null
+++ b/START-HERE.txt
@@ -0,0 +1,17 @@
+START HERE / EMPIEZA AQUÍ
+
+1. Install Xcode from the Mac App Store and open it once.
+ Instala Xcode desde App Store y ábrelo una vez.
+
+2. Double-click START-HERE.command in this folder.
+ Abre START-HERE.command en esta carpeta con doble clic.
+
+It downloads missing tools, builds your own app and opens it.
+Descarga las herramientas que faltan, compila tu app y la abre.
+
+3. Choose iPhone / iPad or Mac, then your own game and keys folders.
+ Elige iPhone / iPad o Mac y las carpetas de tu juego y tus llaves.
+
+App location / Dónde queda: ~/Applications/nxnative Easy.app
+Requires / Necesitas: Apple silicon (M1+), macOS 26+, full Xcode.
+No games or keys included / No incluye juegos ni llaves.
diff --git a/desktop/NxNativeEasy/Localization.swift b/desktop/NxNativeEasy/Localization.swift
index 2b3cf74..977ebcf 100644
--- a/desktop/NxNativeEasy/Localization.swift
+++ b/desktop/NxNativeEasy/Localization.swift
@@ -12,6 +12,18 @@ enum EasyLanguage: String, CaseIterable {
struct EasyStrings {
static let english: [String:String] = [
+ "¿Dónde quieres jugar?": "Where do you want to play?",
+ "Elige un dispositivo": "Choose a device",
+ "Elige dónde quieres jugar y añade tus archivos.": "Choose where to play, then add your own files.",
+ "Creamos una app para jugar en tu Mac.": "We create an app to play on your Mac.",
+ "Terminó, pero no encontramos la app.": "Finished, but the app could not be found.",
+ "Tu juego está listo para Mac.": "Your game is ready for Mac.",
+ "Listo. Juega en tu Mac.": "Ready. Play on your Mac.",
+ "Listo. Instálalo en tu iPhone / iPad.": "Ready. Install it on your iPhone / iPad.",
+ "Abrir juego": "Open game",
+ "Compilando la app para Mac…": "Building the Mac app…",
+ "Mostrar app": "Show app",
+ "Tus partidas se guardan en este Mac y se conservan al crear otra versión.": "Saves stay on this Mac and are preserved when you build a new version.",
"Juegos con soporte en la herramienta": "Supported game profiles",
"No incluye ni descarga juegos. Debes añadir tus propios archivos.": "No games are included or downloaded. You must add your own files.",
"Carpeta de tu juego": "Your game folder",
diff --git a/desktop/NxNativeEasy/main.swift b/desktop/NxNativeEasy/main.swift
index 2dc24d3..d7bae35 100644
--- a/desktop/NxNativeEasy/main.swift
+++ b/desktop/NxNativeEasy/main.swift
@@ -8,6 +8,10 @@ import Darwin
didSet {language.save()}
}
func text(_ source: String) -> String {EasyStrings.text(source, language:language)}
+ @Published var target = "" {
+ didSet { output=nil;assets=nil;failed=false;restoreOutput() }
+ }
+ var isMac: Bool {target == "mac"}
@Published var folder: URL?
@Published var keysFolder: URL?
@Published var running = false
@@ -25,8 +29,8 @@ import Darwin
private var cancelled = false
let repo: URL
init() {
- let configured = Bundle.main.object(forInfoDictionaryKey:"NxNativeSource") as? String ?? ""
- repo = URL(fileURLWithPath:configured)
+ let configured=Bundle.main.object(forInfoDictionaryKey:"NxNativeSource") as? String ?? ""
+ repo=URL(fileURLWithPath:configured)
refreshCatalog()
if let path=UserDefaults.standard.string(forKey:"keysDirectory"), FileManager.default.fileExists(atPath:path+"/prod.keys") {
keysFolder=URL(fileURLWithPath:path)
@@ -81,7 +85,15 @@ import Darwin
failed=false;phase="Tus llaves están seleccionadas.";detail="Sólo guardamos la ruta en este Mac, nunca su contenido."
}
func restoreOutput() {
- guard let folder,
+ if isMac {
+ guard let folder,
+ let data=try? Data(contentsOf:folder.appendingPathComponent("nxnative-mac.json")),
+ let record=(try? JSONSerialization.jsonObject(with:data)) as? [String:Any],
+ let path=record["app_path"] as? String,
+ FileManager.default.fileExists(atPath:path+"/Contents/MacOS/dmhost") else {return}
+ output=URL(fileURLWithPath:path);return
+ }
+ guard target == "ios", let folder,
let data=try? Data(contentsOf:folder.appendingPathComponent("nxnative-ios.json")),
let record=(try? JSONSerialization.jsonObject(with:data)) as? [String:Any],
let path=record["light_dir"] as? String else {return}
@@ -90,7 +102,7 @@ import Darwin
let ipa=files.first(where:{$0.pathExtension=="ipa"}) { output=ipa;assets=directory.appendingPathComponent("romfs") }
}
func build() {
- guard let folder, !running else {return}
+ guard let folder, !running, !target.isEmpty else {return}
let script=repo.appendingPathComponent("scripts/prepare_ios.sh")
guard FileManager.default.fileExists(atPath:script.path) else {
phase="No encontramos el código de nxnative.";detail="Vuelve a crear la app desde el repositorio con Abrir-interfaz.command.";failed=true;return
@@ -99,7 +111,7 @@ import Darwin
guard let keysFolder else {return}
let task=Process();let pipe=Pipe()
task.executableURL=URL(fileURLWithPath:"/usr/bin/python3")
- var arguments=[repo.appendingPathComponent("desktop/NxNativeEasy/build_session.py").path,script.path,folder.path]
+ var arguments=[repo.appendingPathComponent("desktop/NxNativeEasy/build_session.py").path,script.path,"--target",target,folder.path]
arguments += ["--keys-folder",keysFolder.path]
task.arguments=arguments;task.currentDirectoryURL=repo
task.standardOutput=pipe;task.standardError=pipe
@@ -124,7 +136,7 @@ import Darwin
} else if status==0 {
self.restoreOutput()
self.failed=self.output==nil
- self.phase=self.output == nil ? "Terminó, pero no encontramos la IPA." : "Tu juego está listo para Sideloadly."
+ self.phase=self.output == nil ? "Terminó, pero no encontramos la app." : (self.isMac ? "Tu juego está listo para Mac." : "Tu juego está listo para Sideloadly.")
self.detail=self.output == nil ? "Usa Ver error para revisar lo que ocurrió." : "Abre la IPA y arrástrala a Sideloadly. La firma se hace con tu cuenta personal."
} else {
self.failed=true;self.phase="No pudimos preparar el juego."
@@ -150,7 +162,7 @@ import Darwin
guard running else {return}
let lower=text.lowercased()
if lower.contains("pack ipa") || lower.contains("source ipa:") || lower.contains("procesados") || lower.contains("processed ") {phase="Creando tu IPA ligera…"}
- else if lower.contains("xcodebuild") || lower.contains("building the ipa") {phase="Compilando la app para iPhone…"}
+ else if lower.contains("xcodebuild") || lower.contains("building the ipa") {phase=isMac ? "Compilando la app para Mac…" : "Compilando la app para iPhone…"}
else if lower.contains("shader") {phase="Preparando los gráficos del juego…"}
else if lower.contains("extract") {phase="Preparando los archivos del juego…"}
}
@@ -160,6 +172,7 @@ import Darwin
if kill(-process.processIdentifier,SIGTERM) != 0 {process.terminate()}
phase="Cancelando…"
}
+ func openMacGame() {if let output, isMac {NSWorkspace.shared.open(output)}}
func revealIPA() {if let output {NSWorkspace.shared.activateFileViewerSelecting([output])}}
func revealAssets() {if let assets {NSWorkspace.shared.activateFileViewerSelecting([assets])}}
var sideloadlyURL: URL? {NSWorkspace.shared.urlForApplication(withBundleIdentifier:"io.sideloadly.sideloadly")}
@@ -200,6 +213,8 @@ struct SupportCarousel: View {
}.offset(x:-offset).frame(width:geometry.size.width,alignment:.leading)
}
}.frame(height:103).clipped()
+ .accessibilityElement(children:.ignore)
+ .accessibilityLabel(games.map(\.name).joined(separator:", "))
.onHover { hovering in
if hovering { pausedAt = Date() }
else if let pause = pausedAt {
@@ -222,7 +237,7 @@ struct CompactWindow: NSViewRepresentable {
adjusted=true
DispatchQueue.main.async {
let width=max(760,window.contentLayoutRect.width)
- window.setContentSize(NSSize(width:width,height:600))
+ window.setContentSize(NSSize(width:width,height:650))
}
}
}
@@ -259,7 +274,7 @@ struct ContentView: View {
}
VStack(alignment:.leading,spacing:6) {
Text(model.text("Prepara tu juego.")).font(.system(size:32,weight:.bold,design:.rounded)).foregroundStyle(ink)
- Text(model.text("Creamos la IPA. La instalas en tu iPhone con Sideloadly."))
+ Text(model.text(model.isMac ? "Creamos una app para jugar en tu Mac." : "Elige dónde quieres jugar y añade tus archivos."))
.font(.system(size:14)).foregroundStyle(.secondary)
}
VStack(alignment:.leading,spacing:10) {
@@ -277,6 +292,11 @@ struct ContentView: View {
SupportCarousel(games:model.catalog)
}
VStack(alignment:.leading,spacing:18) {
+ Picker(model.text("¿Dónde quieres jugar?"),selection:$model.target) {
+ Text(model.text("Elige un dispositivo")).tag("")
+ Text("iPhone / iPad").tag("ios")
+ Text("Mac").tag("mac")
+ }.pickerStyle(.menu).disabled(model.running)
HStack(spacing:12) {
Image(systemName:"folder.fill").font(.system(size:24)).foregroundStyle(lime)
VStack(alignment:.leading,spacing:4) {
@@ -318,13 +338,24 @@ struct ContentView: View {
Spacer()
Image(systemName:"arrow.right")
}.font(.system(size:15,weight:.bold)).padding(15).foregroundStyle(Color.black)
- .background(model.folder == nil ? Color.gray : lime,in:RoundedRectangle(cornerRadius:10))
- }.buttonStyle(.plain).disabled(model.folder == nil)
+ .background(model.folder == nil || model.target.isEmpty ? Color.gray : lime,in:RoundedRectangle(cornerRadius:10))
+ }.buttonStyle(.plain).disabled(model.folder == nil || model.target.isEmpty)
}
}.padding(22).background(Color.white.opacity(0.045),in:RoundedRectangle(cornerRadius:16))
if model.output != nil && !model.running && !model.failed {
VStack(alignment:.leading,spacing:16) {
- Label(model.text("Listo. Instálalo en tu iPhone."),systemImage:"checkmark.circle.fill")
+ if model.isMac {
+ Label(model.text("Listo. Juega en tu Mac."),systemImage:"checkmark.circle.fill")
+ .font(.system(size:20,weight:.semibold)).foregroundStyle(lime)
+ HStack {
+ Button(model.text("Abrir juego")){model.openMacGame()}
+ .buttonStyle(.borderedProminent).tint(lime).foregroundStyle(.black)
+ Button(model.text("Mostrar app")){model.revealIPA()}
+ }
+ Text(model.text("Tus partidas se guardan en este Mac y se conservan al crear otra versión."))
+ .font(.system(size:12)).foregroundStyle(.secondary)
+ } else {
+ Label(model.text("Listo. Instálalo en tu iPhone / iPad."),systemImage:"checkmark.circle.fill")
.font(.system(size:20,weight:.semibold)).foregroundStyle(lime)
Text(model.text("1. Envía la IPA a Sideloadly y pulsa Start con tu cuenta personal."))
.font(.system(size:13))
@@ -337,6 +368,7 @@ struct ContentView: View {
Button(model.text("Mostrar datos del juego")){model.revealAssets()}
Text(model.text("Para actualizar: misma cuenta e identificador de Sideloadly. Instala encima, sin desinstalar."))
.font(.system(size:11)).foregroundStyle(.secondary)
+ }
Button(model.text("Preparar de nuevo")){model.build()}.buttonStyle(.link)
}.padding(22).frame(maxWidth:.infinity,alignment:.leading)
.background(lime.opacity(0.045),in:RoundedRectangle(cornerRadius:16))
@@ -424,6 +456,6 @@ struct GameCatalogView: View {
NSApplication.shared.applicationIconImage=AppArtwork.icon
}
}
- .defaultSize(width:820,height:600)
+ .defaultSize(width:820,height:650)
}
}
diff --git a/docs/images/nxnative-easy-es.png b/docs/images/nxnative-easy-es.png
index 7619018..d925011 100644
Binary files a/docs/images/nxnative-easy-es.png and b/docs/images/nxnative-easy-es.png differ
diff --git a/docs/images/nxnative-easy.png b/docs/images/nxnative-easy.png
index cd8cb1e..0df176c 100644
Binary files a/docs/images/nxnative-easy.png and b/docs/images/nxnative-easy.png differ
diff --git a/docs/personal-ios.md b/docs/personal-ios.md
index b3509bb..9f3a065 100644
--- a/docs/personal-ios.md
+++ b/docs/personal-ios.md
@@ -178,3 +178,18 @@ it does not click Start, select an account, sign or install on a phone. If Sidel
is missing, the action is labelled Get Sideloadly and opens its official website.
The normal build workflow still requires Xcode/Homebrew first and installs other
missing dependencies. No Apple identity is selected by the desktop compiler.
+
+## One-command setup and destination selection
+
+`install.sh` installs missing Homebrew/build tools, prepares an isolated Python
+runtime, compiles the desktop app locally and opens it. Full Xcode must already be
+installed and opened once; the script checks this before downloading anything.
+The install uses an independent source checkout and never resets local changes.
+`--check` reports prerequisite status without installations or network access.
+
+Users must explicitly choose iPhone/iPad or Mac before preparing a game. The iOS
+route preserves the lightweight IPA workflow and `nxnative-ios.json`. The Mac route
+uses the upstream `--platform mac-app` packer with explicit ad-hoc signing, a unique
+output path per build and a separate `nxnative-mac.json` output record. It uses the
+upstream per-title save location without replacing iOS state. Apple silicon is
+required. Successful packaging is not a new claim of gameplay verification.
diff --git a/install.sh b/install.sh
new file mode 100755
index 0000000..141d194
--- /dev/null
+++ b/install.sh
@@ -0,0 +1,62 @@
+#!/bin/bash
+# Local, account-free desktop setup. Never modifies game data or resets a checkout.
+set -euo pipefail
+nxe_check=0
+case "${1:-}" in --check) nxe_check=1;; '') ;; *) echo 'Usage: bash install.sh [--check]' >&2; exit 2;; esac
+[ "$(uname -s)" = Darwin ] || { echo 'This app requires macOS.' >&2; exit 1; }
+[ "$(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; }
+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
+ echo 'First install full Xcode from the Mac App Store and open it once.'
+ echo 'Primero instala Xcode completo desde App Store y ábrelo una vez.'
+ echo 'https://apps.apple.com/app/xcode/id497799835'
+ echo 'Then run this same command again / Después repite este mismo comando.'
+ exit 1
+fi
+export DEVELOPER_DIR="$nxe_developer"
+/usr/bin/xcodebuild -version >/dev/null || { echo 'Open Xcode and finish its first-time setup, then retry.' >&2; exit 1; }
+nxe_sdk=$(/usr/bin/xcrun --sdk macosx --show-sdk-version)
+[ "${nxe_sdk%%.*}" -ge 26 ] || { echo 'Install Xcode 26 or newer, open it once and retry.' >&2; exit 1; }
+nxe_brew=$(command -v brew || true)
+if [ -z "$nxe_brew" ] && [ -x /opt/homebrew/bin/brew ]; then nxe_brew=/opt/homebrew/bin/brew; fi
+if [ "$nxe_check" -eq 1 ]; then
+ echo 'Apple silicon, macOS and Xcode: ready.'
+ if [ -n "$nxe_brew" ]; then echo 'Homebrew: ready.'; else echo 'Homebrew: will be installed by setup.'; fi
+ exit 0
+fi
+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)"
+ nxe_brew=/opt/homebrew/bin/brew
+fi
+[ -x "$nxe_brew" ] || { echo 'Homebrew installation did not complete; retry setup.' >&2; exit 1; }
+export PATH="$("$nxe_brew" --prefix)/bin:$PATH"
+nxe_base=${NXNATIVE_INSTALL_ROOT:-"$HOME/Library/Application Support/nxnative Easy"}
+nxe_source=${NXNATIVE_SOURCE_DIR:-"$nxe_base/Checkout"}
+nxe_tools=${NXNATIVE_TOOLS_DIR:-"$nxe_base/Tools"}
+nxe_app=${NXNATIVE_APP_PATH:-"$HOME/Applications/nxnative Easy.app"}
+mkdir -p "$nxe_base" "$nxe_tools" "$(dirname "$nxe_app")"
+if [ -n "${NXNATIVE_SOURCE_DIR:-}" ]; then
+ [ -f "$nxe_source/scripts/build_desktop_app.py" ] || { echo 'Invalid source checkout.' >&2; exit 1; }
+elif [ ! -e "$nxe_source" ]; then
+ /usr/bin/git clone --depth 1 https://github.com/nalancer08/nxnative-easy.git "$nxe_source"
+elif [ -d "$nxe_source/.git" ]; then
+ nxe_remote=$(/usr/bin/git -C "$nxe_source" remote get-url origin)
+ [ "$nxe_remote" = https://github.com/nalancer08/nxnative-easy.git ] || { echo 'Unexpected checkout origin; preserved without changes.' >&2; exit 1; }
+ if [ -z "$(/usr/bin/git -C "$nxe_source" status --porcelain)" ]; then
+ /usr/bin/git -C "$nxe_source" pull --ff-only
+ else
+ echo 'Local edits preserved: building your existing checkout without updating it.'
+ fi
+else
+ echo 'The setup folder already contains other files; preserved without changes.' >&2; exit 1
+fi
+echo '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…'
+"$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 43d8cbd..d19212e 100755
--- a/scripts/build_desktop_app.py
+++ b/scripts/build_desktop_app.py
@@ -30,7 +30,7 @@ def main():
subprocess.run(['iconutil','-c','icns',str(iconset),'-o',str(resources/'AppIcon.icns')],check=True)
info=dict(CFBundleIconFile='AppIcon', CFBundleIdentifier='com.nalancer08.nxnative.easy',CFBundleName='nxnative',
CFBundleDisplayName='nxnative Easy',CFBundleExecutable='NxNativeEasy',
- CFBundlePackageType='APPL',CFBundleShortVersionString='1.0',CFBundleVersion='1',
+ CFBundlePackageType='APPL',CFBundleShortVersionString='1.1',CFBundleVersion='2',
LSMinimumSystemVersion='26.0',CFBundleDevelopmentRegion='en',CFBundleLocalizations=['en','es'],NSHighResolutionCapable=True,NxNativeSource=str(REPO))
(contents/'Info.plist').write_bytes(plistlib.dumps(info))
# Explicit ad-hoc signing: '-' never selects an Apple certificate or team.
diff --git a/scripts/build_ios.py b/scripts/build_ios.py
index 4f07d96..a49cfd5 100755
--- a/scripts/build_ios.py
+++ b/scripts/build_ios.py
@@ -151,7 +151,7 @@ def main(argv=None):
if settings[key]: command += ['--'+key.replace('_','-'), settings[key]]
command += ['--show-fps' if settings['show_fps'] else '--no-show-fps']
if args.package: command += ['--package',args.package]
- revision = subprocess.run(['git','-C',str(REPO),'rev-parse','HEAD'],capture_output=True,text=True,check=True).stdout.strip()
+ revision = subprocess.run(['git','-C',str(REPO),'rev-parse','HEAD'],capture_output=True,text=True).stdout.strip() or 'downloaded-source'
print('Source checkout:',REPO,flush=True)
print('Source revision:',revision,flush=True)
print('Output:',output,flush=True)
diff --git a/scripts/build_mac.py b/scripts/build_mac.py
new file mode 100644
index 0000000..9d7e8c8
--- /dev/null
+++ b/scripts/build_mac.py
@@ -0,0 +1,52 @@
+#!/usr/bin/env python3
+"""Prepare a self-contained Mac game app from the user's own files."""
+import argparse
+from datetime import datetime
+import json
+from pathlib import Path
+import platform
+import sys
+from build_ios import REPO, build_lock, load_builder, select_keys
+
+
+def main(argv=None):
+ parser=argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('game_dir',type=Path)
+ parser.add_argument('--keys-folder',type=Path,required=True)
+ parser.add_argument('--dry-run',action='store_true')
+ args=parser.parse_args(argv)
+ game=args.game_dir.expanduser().resolve(strict=True)
+ if not game.is_dir() or game==REPO or REPO in game.parents:
+ raise ValueError('Keep game folders outside the source repository.')
+ packages=[p for p in game.iterdir() if p.is_file() and p.suffix.lower() in {'.nsp','.xci','.nsz','.xcz'}]
+ if not packages: raise ValueError('No NSP/XCI/NSZ/XCZ files directly in this game folder.')
+ output=game/('nxnative-mac-'+datetime.now().strftime('%Y%m%d-%H%M%S-%f')+'.app')
+ command=[str(game),'--platform','mac-app','--mac-sign','-','--team-id','0000000000',
+ '--progress','lines','-o',str(output)]
+ if args.dry_run:
+ print('Mac command:',command)
+ return 0
+ if sys.platform!='darwin' or platform.machine()!='arm64':
+ raise ValueError('Mac games require an Apple silicon Mac.')
+ args.keys=None
+ keys=select_keys(args)
+ with build_lock():
+ if any(p.suffix.lower() in {'.nsz','.xcz'} for p in packages):
+ import subprocess
+ subprocess.run([sys.executable,'-m','pip','install','nsz'],check=True)
+ code=load_builder().cli(command+['-k',str(keys)])
+ if code: return code
+ if not (output/'Contents/MacOS/dmhost').is_file():
+ raise ValueError('Build finished without a Mac game app.')
+ record={'schema':1,'app_path':str(output)}
+ state=game/'nxnative-mac.json'
+ temporary=state.with_suffix('.json.tmp')
+ temporary.write_text(json.dumps(record,indent=2)+'\n')
+ temporary.replace(state)
+ print('Mac app:',output,flush=True)
+ return 0
+
+if __name__=='__main__':
+ try: sys.exit(main())
+ except (ValueError,OSError) as error:
+ print('Error:',error,file=sys.stderr);sys.exit(1)
diff --git a/scripts/prepare_ios.sh b/scripts/prepare_ios.sh
index 714b18d..1fa22b4 100755
--- a/scripts/prepare_ios.sh
+++ b/scripts/prepare_ios.sh
@@ -5,10 +5,10 @@ set -euo pipefail
nxi_repo=$(cd "$(dirname "$0")/.." && pwd -P)
if [ "${1:-}" = --help ] || [ "${1:-}" = -h ]; then
cat <<'HELP'
-Usage: bash scripts/prepare_ios.sh [--setup-only] [game-folder] [build options]
+Usage: bash scripts/prepare_ios.sh [--target ios|mac] [--setup-only] [game-folder] [build options]
No folder: opens the macOS folder picker.
Installs missing tools, creates a Python environment beside the game, and builds
-an unsigned lightweight IPA using THIS checkout, including local control edits.
+an unsigned lightweight IPA or Mac game app using THIS checkout.
Build options: --keys-folder FOLDER (your own prod.keys) --keys PATH --language es-419 --bundle-id ID --app-name NAME
--package v327680 --gpu none --full-only --app-only --dry-run
Examples:
@@ -18,6 +18,9 @@ Run scripts/build_ios.py --help for build details.
HELP
exit 0
fi
+nxi_target=ios
+if [ "${1:-}" = --target ]; then nxi_target=${2:-}; shift 2; fi
+case "$nxi_target" in ios|mac) ;; *) echo 'Choose ios or mac.' >&2; exit 1;; esac
nxi_setup=0
if [ "${1:-}" = --setup-only ]; then nxi_setup=1; shift; fi
[ "$(uname -s)" = Darwin ] || { echo 'macOS is required.' >&2; exit 1; }
@@ -34,17 +37,23 @@ if [ ! -d "$nxi_developer/Platforms/iPhoneOS.platform" ]; then nxi_developer=/Ap
[ -d "$nxi_developer/Platforms/iPhoneOS.platform" ] || { echo 'Install and open full Xcode first.' >&2; exit 1; }
export DEVELOPER_DIR="$nxi_developer"
/usr/bin/xcodebuild -version
-nxi_sdk=$(/usr/bin/xcrun --sdk iphoneos --show-sdk-version)
-[ "${nxi_sdk%%.*}" -ge 26 ] || { echo 'iOS SDK 26 or newer is required.' >&2; exit 1; }
+nxi_sdk_name=iphoneos
+if [ "$nxi_target" = mac ]; then
+ [ "$(uname -m)" = arm64 ] || { echo 'Mac games require an Apple silicon Mac.' >&2; exit 1; }
+ nxi_sdk_name=macosx
+else
+ nxi_sdk=$(/usr/bin/xcrun --sdk iphoneos --show-sdk-version)
+ [ "${nxi_sdk%%.*}" -ge 26 ] || { echo 'iOS SDK 26 or newer is required.' >&2; exit 1; }
+fi
nxi_brew=$(command -v brew || true)
if [ -z "$nxi_brew" ] && [ -x /opt/homebrew/bin/brew ]; then nxi_brew=/opt/homebrew/bin/brew; fi
if [ -z "$nxi_brew" ] && [ -x /usr/local/bin/brew ]; then nxi_brew=/usr/local/bin/brew; fi
[ -n "$nxi_brew" ] || { echo 'Install Homebrew from https://brew.sh first.' >&2; exit 1; }
nxi_prefix=$("$nxi_brew" --prefix)
export PATH="$nxi_prefix/bin:$nxi_prefix/sbin:/usr/local/share/dotnet:$PATH"
-if ! /usr/bin/xcrun --sdk iphoneos metal --version; then
+if ! /usr/bin/xcrun --sdk "$nxi_sdk_name" metal --version; then
/usr/bin/xcodebuild -downloadComponent MetalToolchain
- /usr/bin/xcrun --sdk iphoneos metal --version
+ /usr/bin/xcrun --sdk "$nxi_sdk_name" metal --version
fi
for nxi_formula in python@3.13 xcodegen zstd spirv-cross glslang; do
if ! "$nxi_brew" list --versions "$nxi_formula" >/dev/null 2>&1; then "$nxi_brew" install "$nxi_formula"; fi
@@ -65,4 +74,6 @@ if [ "$nxi_setup" -eq 1 ]; then
echo "Ready. Source checkout preserved: $nxi_repo"
exit 0
fi
-exec "$nxi_venv/bin/python" "$nxi_repo/scripts/build_ios.py" "$nxi_game" "$@"
+nxi_builder=build_ios.py
+if [ "$nxi_target" = mac ]; then nxi_builder=build_mac.py; fi
+exec "$nxi_venv/bin/python" "$nxi_repo/scripts/$nxi_builder" "$nxi_game" "$@"
diff --git a/scripts/tests/test_mac_pipeline.py b/scripts/tests/test_mac_pipeline.py
new file mode 100644
index 0000000..32458b0
--- /dev/null
+++ b/scripts/tests/test_mac_pipeline.py
@@ -0,0 +1,39 @@
+"""Check destination isolation and successful Mac output discovery without a game."""
+import json
+from pathlib import Path
+import sys
+import tempfile
+import unittest
+from unittest.mock import patch
+sys.path.insert(0,str(Path(__file__).resolve().parents[1]))
+import build_mac
+
+class MacPipeline(unittest.TestCase):
+ def test_mac_build_uses_adhoc_and_preserves_ios_state(self):
+ with tempfile.TemporaryDirectory() as temp:
+ root=Path(temp);(root/'base.nsp').write_bytes(b'synthetic')
+ (root/'prod.keys').write_bytes(b'synthetic')
+ (root/'nxnative-ios.json').write_text('{"existing":"preserve"}')
+ calls=[]
+ class Builder:
+ def cli(self,args):
+ calls.append(args)
+ host=Path(args[args.index('-o')+1])/'Contents/MacOS/dmhost'
+ host.parent.mkdir(parents=True);host.write_bytes(b'synthetic')
+ return 0
+ with patch.object(build_mac,'load_builder',return_value=Builder()),patch.object(build_mac.platform,'machine',return_value='arm64'):
+ self.assertEqual(build_mac.main([temp,'--keys-folder',temp]),0)
+ self.assertEqual(calls[0][calls[0].index('--platform')+1],'mac-app')
+ self.assertEqual(calls[0][calls[0].index('--mac-sign')+1],'-')
+ self.assertTrue(Path(json.loads((root/'nxnative-mac.json').read_text())['app_path']).is_dir())
+ self.assertEqual((root/'nxnative-ios.json').read_text(),'{"existing":"preserve"}')
+
+ def test_failure_leaves_previous_mac_record(self):
+ with tempfile.TemporaryDirectory() as temp:
+ root=Path(temp);(root/'base.nsp').write_bytes(b'synthetic');(root/'prod.keys').write_bytes(b'synthetic')
+ (root/'nxnative-mac.json').write_text('{"app_path":"old"}')
+ class Builder:
+ def cli(self,args):return 1
+ with patch.object(build_mac,'load_builder',return_value=Builder()),patch.object(build_mac.platform,'machine',return_value='arm64'):
+ self.assertEqual(build_mac.main([temp,'--keys-folder',temp]),1)
+ self.assertEqual((root/'nxnative-mac.json').read_text(),'{"app_path":"old"}')