diff --git a/LICENSE b/LICENSE index ed93689..af6fd2e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ MIT License Copyright (c) 2026 seedds +Copyright (c) 2026 Erick Sanchez (nxnative Easy additions) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README-comandos.md b/README-comandos.md index e5590f9..6ed70e8 100644 --- a/README-comandos.md +++ b/README-comandos.md @@ -115,15 +115,15 @@ Necesitas: - Un Mac con **Xcode completo** instalado y abierto al menos una vez, y SDK de iOS 26 o posterior. - [Homebrew](https://brew.sh) instalado. El script instala las demás herramientas que falten. - Un iPhone con **A17 Pro o posterior**, o iPad con **M1 o posterior**, y **iOS/iPadOS 26 o posterior**, según los requisitos de esta base. El soporte para otros chips no se incorporó desde los PR revisados. -- Tus archivos del juego y `prod.keys`, en una carpeta fuera del repositorio. +- Tus archivos del juego y `prod.keys`, en carpetas fuera del repositorio. - Espacio para los archivos originales, la extracción, cachés, la IPA completa y otra copia de `romfs`. - [Sideloadly](https://sideloadly.io) para firmar e instalar con tu cuenta Apple personal. En un Mac nuevo, descarga nuestra copia privada. Debes tener acceso con tu cuenta GitHub: ```sh -git clone https://github.com/nalancer08/nxnative-personal.git -cd nxnative-personal +git clone https://github.com/nalancer08/nxnative-easy.git +cd nxnative-easy ``` **Si ya tienes nuestra copia en este Mac, abre Terminal en esa carpeta y omite el clonado.** No clones de nuevo el repositorio original para compilar: esa copia no tendría nuestro editor ni estos scripts. @@ -138,7 +138,7 @@ Desde la carpeta del repositorio: bash scripts/prepare_ios.sh ``` -Selecciona la carpeta que contiene un solo juego con sus actualizaciones. El script pide `prod.keys` si no está en esa carpeta, prepara las herramientas, compila y separa los datos automáticamente. +Selecciona la carpeta que contiene un solo juego con sus actualizaciones. El script te pide seleccionar la carpeta que contiene tu propio `prod.keys`, prepara las herramientas, compila y separa los datos automáticamente. ### Opción 2: indicar la carpeta @@ -148,13 +148,12 @@ Cambia la ruta del ejemplo por la tuya y conserva las comillas: bash scripts/prepare_ios.sh "/Volumes/Juegos/MetroidDread" ``` -La carpeta puede tener esta estructura; no cambies las extensiones de los archivos: +Las llaves se seleccionan desde su propia carpeta y no se incluyen. La carpeta del juego puede tener esta estructura; no cambies las extensiones de los archivos: ```text MetroidDread/ juego-base.nsp actualizacion.nsp - prod.keys ``` También se admiten XCI y, con soporte adicional instalado por el script, NSZ/XCZ. Usa una carpeta distinta para cada juego. Por defecto se selecciona la actualización más reciente presente; puedes fijar una con `--package`. diff --git a/README.md b/README.md index 50a26a5..9485e37 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,21 @@ -# nxnative personal +# nxnative Easy -**Tus juegos, en tu iPhone. Con imágenes y sin comandos para el uso diario.** +**Prepara tus juegos para iPhone desde una app de Mac. Sin comandos para el uso diario.** ![Nuestra app para preparar juegos](docs/images/nxnative-easy.png) +## Qué aporta esta versión + +Partimos de [seedds/nxnative](https://github.com/seedds/nxnative): el motor, los perfiles y la compatibilidad son trabajo del proyecto original. **nxnative Easy** añade: + +- **App para Mac:** elegir juego, avance, cancelación y acceso a IPA, datos y Sideloadly. +- **Controles editables en iPhone:** posición, tamaño y opacidad, guardados por juego y layout. +- **IPA ligera automática:** los datos `romfs` van aparte; se conservan salidas y cachés válidas. +- **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. + ## Abre la app y elige tu juego 1. Abre **nxnative.app**. @@ -13,7 +25,13 @@ La app prepara las herramientas, compila y deja lista la **IPA ligera**. Verás **En nuestro Mac:** la app está disponible como `nxnative.app` en Descargas. Recuerda tu última carpeta y permite volver a mostrar la IPA ya preparada. -**En otro Mac:** después de descargar este repositorio privado, 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 `prod.keys`. Mantén cada juego fuera del repositorio, en una carpeta con su base y actualizaciones. +**En otro Mac:** después de descargar este repositorio privado, 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. + +## Cómo se compila la app de Mac + +El acceso de apertura usa el compilador Swift de tu Xcode. La firma de la app es **ad hoc**, local y sin cuenta, certificado ni equipo Apple (`TeamIdentifier` no asignado). No usa cuentas de empresa. Sólo al instalar la **IPA de iPhone con Sideloadly** introduces tu cuenta personal, dentro de Sideloadly. + +Los símbolos de la interfaz son genéricos del sistema; no se usan imágenes ni logotipos de los juegos en la app. La galería de documentación de abajo procede de upstream y está separada de la interfaz. ## Instalar: Sideloadly + Finder diff --git a/desktop/NxNativeEasy/main.swift b/desktop/NxNativeEasy/main.swift index 5bbb48f..663cf6b 100644 --- a/desktop/NxNativeEasy/main.swift +++ b/desktop/NxNativeEasy/main.swift @@ -6,11 +6,13 @@ import Darwin struct GameCard: Identifiable { let id: String let name: String - let image: String + let symbol: String + let tint: Color } @MainActor final class BuildModel: ObservableObject { @Published var folder: URL? + @Published var keysFolder: URL? @Published var running = false @Published var phase = "Tu próximo juego empieza aquí." @Published var detail = "Elige su carpeta. Nosotros preparamos la IPA ligera." @@ -24,16 +26,17 @@ struct GameCard: Identifiable { private var cancelled = false let repo: URL let cards: [GameCard] = [ - .init(id:"dread",name:"Metroid Dread",image:"metroid-dread.jpg"), - .init(id:"wonder",name:"Super Mario Bros. Wonder",image:"super-mario-bros-wonder.jpg"), - .init(id:"zelda",name:"Zelda: Breath of the Wild",image:"zelda-breath-of-the-wild.jpg"), - .init(id:"odyssey",name:"Super Mario Odyssey",image:"super-mario-odyssey.jpg"), - .init(id:"cuphead",name:"Cuphead",image:"cuphead.jpg"), - .init(id:"hades",name:"Hades",image:"hades.jpg") + .init(id:"dread",name:"Metroid Dread",symbol:"scope",tint:.orange), + .init(id:"wonder",name:"Super Mario Bros. Wonder",symbol:"sparkles",tint:.mint), + .init(id:"zelda",name:"Zelda: Breath of the Wild",symbol:"mountain.2",tint:.cyan), + .init(id:"odyssey",name:"Super Mario Odyssey",symbol:"globe.americas",tint:.red) ] init() { let configured = Bundle.main.object(forInfoDictionaryKey:"NxNativeSource") as? String ?? "" repo = URL(fileURLWithPath:configured) + if let path=UserDefaults.standard.string(forKey:"keysDirectory"), FileManager.default.fileExists(atPath:path+"/prod.keys") { + keysFolder=URL(fileURLWithPath:path) + } if let path = UserDefaults.standard.string(forKey:"lastGameFolder"), FileManager.default.fileExists(atPath:path) { folder=URL(fileURLWithPath:path) restoreOutput() @@ -60,6 +63,21 @@ struct GameCard: Identifiable { failed=false;output=nil;assets=nil;transcript="";restoreOutput() } catch { phase="No pudimos leer esa carpeta.";detail=error.localizedDescription;failed=true } } + func chooseKeysFolder() { + let panel=NSOpenPanel();panel.canChooseDirectories=true;panel.canChooseFiles=false + panel.allowsMultipleSelection=false;panel.prompt="Usar mis llaves" + panel.message="Elige tu carpeta con prod.keys. No copiamos las llaves ni las incluimos en la app." + guard panel.runModal() == .OK, let url=panel.url else {return} + let resolved=url.resolvingSymlinksInPath().standardizedFileURL + let root=repo.resolvingSymlinksInPath().standardizedFileURL + let key=resolved.appendingPathComponent("prod.keys") + guard resolved != root, !resolved.path.hasPrefix(root.path+"/"), + FileManager.default.isReadableFile(atPath:key.path) else { + failed=true;phase="Elige una carpeta externa con tu prod.keys.";detail="No se proporcionan llaves. Debes seleccionar las tuyas.";return + } + keysFolder=resolved;UserDefaults.standard.set(resolved.path,forKey:"keysDirectory") + 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, let data=try? Data(contentsOf:folder.appendingPathComponent("nxnative-ios.json")), @@ -75,16 +93,13 @@ struct GameCard: Identifiable { 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 } - var keys: URL? - if !appOnly && !FileManager.default.fileExists(atPath:folder.appendingPathComponent("prod.keys").path) { - let panel=NSOpenPanel();panel.prompt="Elegir prod.keys";panel.message="Sólo la primera vez: selecciona tus llaves. Se quedan en tu Mac." - guard panel.runModal() == .OK else {return};keys=panel.url - } + if !appOnly && keysFolder == nil {chooseKeysFolder()} + guard appOnly || keysFolder != nil 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] if appOnly { arguments.append("--app-only") } - if let keys {arguments += ["--keys",keys.path]} + if !appOnly, let keysFolder {arguments += ["--keys-folder",keysFolder.path]} task.arguments=arguments;task.currentDirectoryURL=repo task.standardOutput=pipe;task.standardError=pipe var environment=ProcessInfo.processInfo.environment @@ -154,7 +169,7 @@ struct ContentView: View { ScrollView { VStack(alignment:.leading,spacing:24) { HStack { - Label("nxnative / personal",systemImage:"gamecontroller.fill").font(.system(size:15,weight:.semibold,design:.monospaced)).foregroundStyle(lime) + Label("nxnative / easy",systemImage:"gamecontroller.fill").font(.system(size:15,weight:.semibold,design:.monospaced)).foregroundStyle(lime) Spacer() Text("HECHO PARA TU IPHONE").font(.system(size:10,weight:.medium,design:.monospaced)).tracking(2).foregroundStyle(.gray) } @@ -165,9 +180,10 @@ struct ContentView: View { HStack(spacing:12) { ForEach(model.cards.prefix(4)) {card in VStack(alignment:.leading,spacing:8) { - if let image=NSImage(contentsOf:model.repo.appendingPathComponent("docs/images/"+card.image)) { - Image(nsImage:image).resizable().scaledToFill().frame(height:106).clipped().clipShape(RoundedRectangle(cornerRadius:12)) - } + ZStack { + RoundedRectangle(cornerRadius:12).fill(card.tint.opacity(0.12)) + Image(systemName:card.symbol).font(.system(size:38,weight:.light)).foregroundStyle(card.tint) + }.frame(height:106) Text(card.name).font(.system(size:11,weight:.medium)).foregroundStyle(ink).lineLimit(1) }.frame(maxWidth:.infinity) } @@ -183,6 +199,13 @@ struct ContentView: View { Spacer() Button(model.folder == nil ? "1. Elegir juego" : "Cambiar") {model.chooseFolder()}.disabled(model.running) } + HStack { + Image(systemName:"key.horizontal").foregroundStyle(lime) + Text(model.keysFolder == nil ? "Tus llaves: selecciona su carpeta" : "Tus llaves: carpeta seleccionada") + .font(.system(size:12)).foregroundStyle(ink) + Spacer() + Button(model.keysFolder == nil ? "Elegir carpeta de llaves" : "Cambiar llaves") {model.chooseKeysFolder()}.disabled(model.running) + } Divider().overlay(Color.white.opacity(0.08)) HStack { VStack(alignment:.leading,spacing:5) { @@ -218,7 +241,7 @@ struct ContentView: View { ScrollView {Text(model.transcript).font(.system(size:10,design:.monospaced)).textSelection(.enabled).frame(maxWidth:.infinity,alignment:.leading)}.frame(height:180) } HStack { - Link("Guía de instalación",destination:URL(string:"https://github.com/nalancer08/nxnative-personal#readme")!) + Link("Guía de instalación",destination:URL(string:"https://github.com/nalancer08/nxnative-easy#readme")!) if !model.transcript.isEmpty {Button("Copiar diagnóstico"){NSPasteboard.general.clearContents();NSPasteboard.general.setString(model.transcript,forType:.string)}} } }.padding(.top,10) diff --git a/docs/images/nxnative-easy.png b/docs/images/nxnative-easy.png index 5397d61..bfd2c74 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 79693f7..f963d9a 100644 --- a/docs/personal-ios.md +++ b/docs/personal-ios.md @@ -1,7 +1,7 @@ # Desktop interface For daily use, open `nxnative.app`, choose a game folder and press **Preparar para iPhone**. -The native macOS interface shows progress and provides buttons for the lightweight IPA, +The native macOS interface uses generic SF Symbols instead of game artwork and shows progress and provides buttons for the lightweight IPA, RomFS folder and Sideloadly. First-time setup still needs full Xcode, Homebrew and the user's game/key files. It never signs, installs or enters an Apple account. @@ -12,7 +12,7 @@ CLI remains available below. Cancellation signals the entire build process group # Personal iOS builds -This private branch retains nxnative's MIT license and attribution. It starts from +This branch retains nxnative's MIT license and attribution. It starts from upstream `ecbeae164c0b419d1ba1031248fe1402050a3eb0`, includes the shared iOS touch editor and the tested #277 extraction-space fix, and keeps the working Dread shader chain. Upstream changes are reviewed in [the PR review](personal-pr-review.md). @@ -20,7 +20,7 @@ chain. Upstream changes are reviewed in [the PR review](personal-pr-review.md). ## One command, including lightweight export Double-click `scripts/Build-iPhone.command` in Finder, choose the folder containing -one game's base NSP/XCI and updates, and select your own `prod.keys` when asked. +one game's base NSP/XCI and updates, and select the external folder containing your own `prod.keys` when asked. Alternatively, from this checkout: ```sh @@ -83,7 +83,7 @@ The direct Python build invocation needs the prepared venv. For example use `/nxnative-tools/venv/bin/python` instead of `python3`. Dry-run reads the folder/configuration and source revision without opening keys or running a build; the setup shell still checks/installs dependencies before invoking Python. -`--full-only` omits RomFS export. `--keys`, `--bundle-id`, `--app-name`, `--language`, +`--full-only` omits RomFS export. `--keys-folder` (or an explicit `--keys` file path), `--bundle-id`, `--app-name`, `--language`, `--package` and `--gpu` provide explicit overrides. The wrapper rejects simultaneous builds from this checkout because generated files and caches are shared. Avoid launching the original upstream builder alongside it. @@ -125,3 +125,15 @@ outside this source repository. `.gitignore` guards common output types; it is not a substitute for reviewing staged files. Original downloaded scripts were preserved on the Mac. These maintained scripts generalize that preparation and splitting workflow without the Metroid-only profile/bundle assumptions. + +## Accounts and keys + +The desktop build runs swiftc and explicitly ad-hoc signs with `codesign --sign -`. +No Apple identity, certificate or development team is selected. The iPhone package +continues to use CODE_SIGNING_ALLOWED=NO; Sideloadly signing is a separate user step. +The GUI remembers only a user-selected keys-folder path in local UserDefaults. +It passes that explicit folder to the builder; keys are not copied, bundled, +downloaded or stored in repository configuration. The CLI always prompts for a +folder when no explicit key option is supplied, even if prod.keys is beside a game. +The game app's key file is never packed into an IPA. Tests use disposable synthetic +bytes, not Nintendo key material. diff --git a/docs/publication-notes.md b/docs/publication-notes.md new file mode 100644 index 0000000..9f2f5ed --- /dev/null +++ b/docs/publication-notes.md @@ -0,0 +1,43 @@ +# Publication preparation + +nxnative Easy is derived from [seedds/nxnative](https://github.com/seedds/nxnative). +The original engine, game profiles and compatibility work retain their authors, +MIT notice and third-party license files. Our additions are the native desktop +preparation UI, persistent iOS touch-control editor and lightweight build workflow. +No endorsement by Nintendo, Apple, game publishers or upstream authors is implied. + +The desktop UI uses generic system symbols rather than game screenshots or logos. +The README/gallery and upstream documentation still contain game screenshots; +removing artwork from the app does not remove that documentation or historical +Git objects. The code's license is not a blanket grant of rights over third-party +artwork. Publication is a separate owner decision; this check is not legal clearance. + +The current local desktop build uses Xcode's Swift compiler and explicit ad-hoc +signing (`codesign --sign - --timestamp=none`). `Signature=adhoc` and an unset +`TeamIdentifier` confirm no Apple development identity is selected. No Apple +account, organization certificate, provisioning profile or company team is used. +The generated iPhone IPA remains unsigned; Sideloadly signing is performed later +by the user with their own personal account. + +Users must select their own external folder containing prod.keys. The application +stores only that local folder path in UserDefaults, not key values. The maintained +CLI does not discover keys beside a game without an explicit selection. Keys, +game packages, saves, IPAs and build outputs are excluded from this repository. +Synthetic test fixtures are created in temporary directories and contain dummy +bytes, never usable console keys. + +Pre-publication audit on 2026-10-26 inspected 10,172 objects reachable from main, +including 5,877 historical blobs (5,699 treated as text). Checks covered sensitive +filenames, private PEM headers, recognizable GitHub tokens, console-key assignment +patterns, company references and the owner's known local home/email identifiers. +No matches were found. This targeted scan is not proof that no possible secret +exists. It does not audit unreachable server objects or independently establish +third-party copyright permissions. + +Validation: native desktop compilation, explicit ad-hoc signature inspection, +10 script/worker tests and 4 documentation/link checks passed. No Apple account +was used, no game was signed/installed, and no new device gameplay is claimed. + +See GitHub's [visibility documentation](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/managing-repository-settings/setting-repository-visibility): +public visibility exposes repository content and Actions history, and others can fork +it. A later return to private does not retrieve copies already made by others. diff --git a/scripts/build_desktop_app.py b/scripts/build_desktop_app.py index 9d6d442..345a126 100755 --- a/scripts/build_desktop_app.py +++ b/scripts/build_desktop_app.py @@ -25,6 +25,8 @@ def main(): CFBundlePackageType='APPL',CFBundleShortVersionString='1.0',CFBundleVersion='1', LSMinimumSystemVersion='26.0',NSHighResolutionCapable=True,NxNativeSource=str(REPO)) (contents/'Info.plist').write_bytes(plistlib.dumps(info)) + # Explicit ad-hoc signing: '-' never selects an Apple certificate or team. + subprocess.run(['codesign','--force','--sign','-','--timestamp=none',str(output)],check=True) if args.open:subprocess.run(['open',str(output)],check=True) print(output) diff --git a/scripts/build_ios.py b/scripts/build_ios.py index 13206e1..4f07d96 100755 --- a/scripts/build_ios.py +++ b/scripts/build_ios.py @@ -63,10 +63,35 @@ def verify_ipa(path): return info +def choose_keys_folder(): + result=subprocess.run(['/usr/bin/osascript','-e', + 'POSIX path of (choose folder with prompt "Select your own folder containing prod.keys")'], + capture_output=True,text=True,check=True) + return Path(result.stdout.rstrip('\n')) + + +def select_keys(args): + # Never silently discover keys next to a game or in a bundled/default location. + folder=args.keys_folder + if args.keys is not None: + keys=args.keys.expanduser() + else: + folder=folder if folder is not None else choose_keys_folder() + keys=folder.expanduser()/'prod.keys' + resolved=keys.resolve() + if REPO==resolved or REPO in resolved.parents: + raise ValueError('Select your own keys outside the source repository.') + if not resolved.is_file() or not resolved.stat().st_size: + raise ValueError('The selected folder must contain your own readable prod.keys.') + return resolved + + def parser(): p = argparse.ArgumentParser(description=__doc__) p.add_argument('game_dir', nargs='?', type=Path, help='folder containing one game and its updates') - p.add_argument('-k', '--keys', type=Path, help='prod.keys path; defaults to game_dir/prod.keys') + keys=p.add_mutually_exclusive_group() + keys.add_argument('--keys-folder',type=Path,help='explicit external folder containing your own prod.keys') + keys.add_argument('-k','--keys',type=Path,help='explicit path to your own prod.keys (legacy CLI option)') p.add_argument('-l', '--language', help='first build defaults to es-419; later builds retain identity settings') p.add_argument('--bundle-id', help='use the SAME effective identifier when updating an installed app') p.add_argument('--app-name') @@ -155,13 +180,7 @@ def main(argv=None): if settings[setting]: env[variable]=settings[setting] subprocess.run(['bash',str(REPO/'dm/ios/tools/build_ipa.sh'),str(output)],cwd=REPO,env=env,check=True) else: - keys = (args.keys or game/'prod.keys').expanduser() - if not keys.is_file() or not keys.stat().st_size: - result = subprocess.run(['/usr/bin/osascript','-e','POSIX path of (choose file with prompt "Select prod.keys")'],capture_output=True,text=True,check=True) - keys = Path(result.stdout.rstrip('\n')) - if not keys.is_file() or not keys.stat().st_size: raise ValueError('Missing prod.keys.') - if keys.resolve() == REPO or REPO in keys.resolve().parents: - raise ValueError('Keep keys outside the source repository.') + keys = select_keys(args) if any(p.suffix.lower() in {'.nsz','.xcz'} for p in packages): subprocess.run([sys.executable,'-m','pip','install','nsz'],check=True) builder = load_builder() diff --git a/scripts/prepare_ios.sh b/scripts/prepare_ios.sh index f742304..9d069c3 100755 --- a/scripts/prepare_ios.sh +++ b/scripts/prepare_ios.sh @@ -9,7 +9,7 @@ Usage: bash scripts/prepare_ios.sh [--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. -Build options: --keys PATH --language es-419 --bundle-id ID --app-name NAME +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: bash scripts/prepare_ios.sh /Volumes/Games/MetroidDread diff --git a/scripts/tests/test_personal_pipeline.py b/scripts/tests/test_personal_pipeline.py index 42b68df..1b66f89 100644 --- a/scripts/tests/test_personal_pipeline.py +++ b/scripts/tests/test_personal_pipeline.py @@ -64,7 +64,7 @@ class Pipeline(unittest.TestCase): def cli(self,args): calls.append(args);fixture(Path(args[args.index('-o')+1]));return 0 with patch.object(build_ios,'load_builder',return_value=Builder()): - self.assertEqual(build_ios.main([temp]),0) + self.assertEqual(build_ios.main([temp,"--keys-folder",temp]),0) state=json.loads((game/'nxnative-ios.json').read_text()) self.assertEqual(state['bundle_id'],'org.nxnative.test') self.assertTrue(list(Path(state['light_dir']).glob('*.ipa'))) @@ -73,6 +73,21 @@ class Pipeline(unittest.TestCase): with self.assertRaises(ValueError):build_ios.main([temp,'--bundle-id','changed','--dry-run']) self.assertEqual(calls[0][calls[0].index('--team-id')+1],'0000000000') + def test_keys_require_explicit_folder_selection(self): + with tempfile.TemporaryDirectory() as temp: + folder=Path(temp);(folder/'prod.keys').write_bytes(b'synthetic') + args=build_ios.parser().parse_args([temp]) + with patch.object(build_ios,'choose_keys_folder',return_value=folder) as picker: + selected=build_ios.select_keys(args) + picker.assert_called_once() + self.assertEqual(selected,folder.resolve()/'prod.keys') + self.assertEqual((folder/'prod.keys').read_bytes(),b'synthetic') + + def test_missing_selected_keys_rejected(self): + with tempfile.TemporaryDirectory() as temp: + args=build_ios.parser().parse_args([temp,'--keys-folder',temp]) + with self.assertRaises(ValueError):build_ios.select_keys(args) + def test_parallel_build_is_rejected(self): with build_ios.build_lock(): with self.assertRaises(ValueError):