Add one-command guided setup and Mac game builds alongside iPhone and iPad
This commit is contained in:
1 parent
25db760cd3
commit
1b5ecaabb5
16 files changed
+334
-42
No files matched your search
@@ -29,3 +29,5 @@ exefs/
|
||||
save/
|
||||
Payload/
|
||||
nxnative-ios.json
|
||||
|
||||
nxnative-mac.json
|
||||
+33
-10
@@ -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.
|
||||
|
||||

|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
nxe_here=$(cd "$(dirname "$0")" && pwd -P)
|
||||
/bin/bash "$nxe_here/install.sh"
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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 |
@@ -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
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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" "$@"
|
||||
@@ -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"}')
|
||||
Reference in new issue
Block a user