Add one-command guided setup and Mac game builds alongside iPhone and iPad

This commit is contained in:
Erick Sanchez committed 2026-10-26 12:57:20 -06:00
1 parent 25db760cd3
commit 1b5ecaabb5
16 files changed
+334 -42

No files matched your search

+2
View File
@@ -29,3 +29,5 @@ exefs/
save/
Payload/
nxnative-ios.json
nxnative-mac.json
+33 -10
View File
@@ -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/<title ID>/`.
**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.
![Nuestra app para preparar juegos](docs/images/nxnative-easy-es.png)
@@ -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
+33 -10
View File
@@ -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/<title ID>/`.
**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.
![The nxnative Easy desktop app](docs/images/nxnative-easy.png)
@@ -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
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
set -euo pipefail
nxe_here=$(cd "$(dirname "$0")" && pwd -P)
/bin/bash "$nxe_here/install.sh"
+17
View File
@@ -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.
+12
View File
@@ -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",
+45 -13
View File
@@ -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)
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 413 KiB

After

Width:  |  Height:  |  Size: 423 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 392 KiB

After

Width:  |  Height:  |  Size: 119 KiB

+15
View File
@@ -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.
Executable
+62
View File
@@ -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
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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)
+52
View File
@@ -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)
+18 -7
View File
@@ -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" "$@"
+39
View File
@@ -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"}')