From 8d04c7f85af1638858985ba70bbd362963f3c8da Mon Sep 17 00:00:00 2001 From: Duy Tran Khanh Date: Thu, 11 Feb 2021 17:11:56 +0700 Subject: [PATCH] Implemented Microsoft authentication --- .../microsoft/MicrosoftAuthTask.java | 55 ++++ .../microsoft/MicrosoftAuthTask.java.todo | 311 ------------------ .../microsoft/{Msa.java.todo => Msa.java} | 0 .../yggdrasil/YggdrasilAuthenticator.java | 2 +- .../net/kdt/pojavlaunch/uikit/AccountJNI.java | 7 +- Natives/AppDelegate.m | 6 + Natives/CMakeLists.txt | 1 + Natives/Info.plist | 11 + Natives/LoginViewController.m | 28 +- Natives/SceneDelegate.m | 7 + Natives/aminimake.sh | 2 +- 11 files changed, 111 insertions(+), 319 deletions(-) create mode 100644 JavaApp/src/main/java/net/kdt/pojavlaunch/authenticator/microsoft/MicrosoftAuthTask.java delete mode 100644 JavaApp/src/main/java/net/kdt/pojavlaunch/authenticator/microsoft/MicrosoftAuthTask.java.todo rename JavaApp/src/main/java/net/kdt/pojavlaunch/authenticator/microsoft/{Msa.java.todo => Msa.java} (100%) diff --git a/JavaApp/src/main/java/net/kdt/pojavlaunch/authenticator/microsoft/MicrosoftAuthTask.java b/JavaApp/src/main/java/net/kdt/pojavlaunch/authenticator/microsoft/MicrosoftAuthTask.java new file mode 100644 index 00000000..503dbf06 --- /dev/null +++ b/JavaApp/src/main/java/net/kdt/pojavlaunch/authenticator/microsoft/MicrosoftAuthTask.java @@ -0,0 +1,55 @@ +package net.kdt.pojavlaunch.authenticator.microsoft; + +import android.util.*; + +import java.net.*; +import java.text.*; +import java.util.*; +import net.kdt.pojavlaunch.*; +import net.kdt.pojavlaunch.authenticator.mojang.*; +import net.kdt.pojavlaunch.authenticator.microsoft.*; +import org.json.*; + +import java.text.ParseException; +import java.io.*; +import net.kdt.pojavlaunch.value.launcherprofiles.*; +import net.kdt.pojavlaunch.value.*; + +public class MicrosoftAuthTask extends AsyncTask { + + //private Gson gson = new Gson(); + + public Object run(String... args) throws Throwable { + /* + publishProgress(); + String msaAccessToken = acquireAccessToken(args[0]); + + publishProgress(); + String xblToken = acquireXBLToken(msaAccessToken); + + publishProgress(); + String[] xstsData = acquireXsts(xblToken); + + publishProgress(); + String mcAccessToken = acquireMinecraftToken(xstsData[0], xstsData[1]); + + publishProgress(); + + */ + Msa msa = new Msa(this, Boolean.parseBoolean(args[0]), args[1]); + + MinecraftAccount acc = new MinecraftAccount(); + if (msa.doesOwnGame) { + acc.clientToken = "0"; /* FIXME */ + acc.accessToken = msa.mcToken; + acc.username = msa.mcName; + acc.profileId = msa.mcUuid; + acc.isMicrosoft = true; + acc.msaRefreshToken = msa.msRefreshToken; + } + acc.save(); + + return acc; + } +} + diff --git a/JavaApp/src/main/java/net/kdt/pojavlaunch/authenticator/microsoft/MicrosoftAuthTask.java.todo b/JavaApp/src/main/java/net/kdt/pojavlaunch/authenticator/microsoft/MicrosoftAuthTask.java.todo deleted file mode 100644 index a5a8fdef..00000000 --- a/JavaApp/src/main/java/net/kdt/pojavlaunch/authenticator/microsoft/MicrosoftAuthTask.java.todo +++ /dev/null @@ -1,311 +0,0 @@ -package net.kdt.pojavlaunch.authenticator.microsoft; - -import android.app.*; -import android.content.*; -import android.os.*; -import android.util.*; - -import java.net.*; -import java.text.*; -import java.util.*; -import net.kdt.pojavlaunch.*; -import net.kdt.pojavlaunch.authenticator.mojang.*; -import net.kdt.pojavlaunch.authenticator.microsoft.*; -import org.json.*; - -import java.text.ParseException; -import java.io.*; -import net.kdt.pojavlaunch.value.launcherprofiles.*; -import net.kdt.pojavlaunch.value.*; - -public class MicrosoftAuthTask extends AsyncTask { -/* - private static final String authTokenUrl = "https://login.live.com/oauth20_token.srf"; - private static final String xblAuthUrl = "https://user.auth.xboxlive.com/user/authenticate"; - private static final String xstsAuthUrl = "https://xsts.auth.xboxlive.com/xsts/authorize"; - private static final String mcLoginUrl = "https://api.minecraftservices.com/authentication/login_with_xbox"; - private static final String mcStoreUrl = "https://api.minecraftservices.com/entitlements/mcstore"; - private static final String mcProfileUrl = "https://api.minecraftservices.com/minecraft/profile"; -*/ - - //private Gson gson = new Gson(); - private RefreshListener listener; - - private Context ctx; - private ProgressDialog build; - - public MicrosoftAuthTask(Context ctx, RefreshListener listener) { - this.ctx = ctx; - this.listener = listener; - } - - @Override - public void onPreExecute() { - build = new ProgressDialog(ctx); - build.setMessage(ctx.getString(R.string.global_waiting)); - build.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); - build.setCancelable(false); - build.setMax(6); - build.show(); - } - - @Override - public Object doInBackground(String... args) { - try { - /* - publishProgress(); - String msaAccessToken = acquireAccessToken(args[0]); - - publishProgress(); - String xblToken = acquireXBLToken(msaAccessToken); - - publishProgress(); - String[] xstsData = acquireXsts(xblToken); - - publishProgress(); - String mcAccessToken = acquireMinecraftToken(xstsData[0], xstsData[1]); - - publishProgress(); - - */ - Msa msa = new Msa(this, Boolean.parseBoolean(args[0]), args[1]); - - MinecraftAccount acc = new MinecraftAccount(); - if (msa.doesOwnGame) { - acc.clientToken = "0"; /* FIXME */ - acc.accessToken = msa.mcToken; - acc.username = msa.mcName; - acc.profileId = msa.mcUuid; - acc.isMicrosoft = true; - acc.msaRefreshToken = msa.msRefreshToken; - } - acc.save(); - - return acc; - } catch (Throwable e) { - return e; - } - } - - public void publishProgressPublic() { - super.publishProgress(); - } - - @Override - protected void onProgressUpdate(Void[] p1) { - super.onProgressUpdate(p1); - build.setProgress(build.getProgress() + 1); - } - - @Override - public void onPostExecute(Object result) { - build.dismiss(); - if (result instanceof MinecraftAccount) { - listener.onSuccess((MinecraftAccount) result); - } else { - listener.onFailed((Throwable) result); - } - } - - // Based on https://github.com/MiniDigger/MiniLauncher/blob/master/launcher/src/main/java/me/minidigger/minecraftlauncher/launcher/gui/MsaFragmentController.java - /* - MIT License - - Copyright (c) 2018 Ammar Ahmad - Copyright (c) 2018 Martin Benndorf - Copyright (c) 2018 Mark Vainomaa - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - /* - private final class XSTSXUI { - private String uhs; - } - - private final class XSTSDisplayClaims { - private XSTSXUI[] xui; - } - - private final class GlobalToken { - // MSA AccessToken - private String access_token; - - // XBL, XSTS Token - private String Token; - - // XSTS - private XSTSDisplayClaims DisplayClaims; - - // Minecraft side - private String id; - private String name; - } - - private String acquireAccessToken(String authcode) throws IOException, URISyntaxException{ - URI uri = new URI(authTokenUrl); - - Map data = new ArrayMap<>(); - data.put("client_id", "00000000402b5328"); - data.put("code", authcode); - data.put("grant_type", "authorization_code"); - data.put("redirect_url", "https://login.live.com/oauth20_desktop.srf"); - data.put("scope", "service::user.auth.xboxlive.com::MBI_SSL"); - - HttpRequest request = HttpRequest.newBuilder(uri) - .header("Content-Type", "application/x-www-form-urlencoded") - .POST(ofFormData(data)).build(); - - HttpResponse resp = HttpClient.newBuilder().build().sendRequest(request, HttpResponse.BodyHandlers.ofString()); - if (resp.statusCode() >= 200 && resp.statusCode() < 300) { - return Tools.GLOBAL_GSON.fromJson((String) resp.body(), GlobalToken.class).access_token; - } else { - throw new RuntimeException("Error " + resp.statusCode() + ": " + (String) resp.body()); - } - } - - private String acquireXBLToken(String accessToken) throws IOException, URISyntaxException { - URI uri = new URI(xblAuthUrl); - - Map dataProp = new ArrayMap<>(); - dataProp.put("AuthMethod", "RPS"); - dataProp.put("SiteName", "user.auth.xboxlive.com"); - dataProp.put("RpsTicket", accessToken); - - Map data = new ArrayMap<>(); - data.put("Properties", dataProp); - data.put("RelyingParty", "http://auth.xboxlive.com"); - data.put("TokenType", "JWT"); - - HttpRequest request = HttpRequest.newBuilder(uri) - .header("Content-Type", "application/json") - .header("Accept", "application/json") - .POST(ofJSONData(data)).build(); - - HttpResponse resp = HttpClient.newBuilder().build().sendRequest(request, HttpResponse.BodyHandlers.ofString()); - if (resp.statusCode() >= 200 && resp.statusCode() < 300) { - return Tools.GLOBAL_GSON.fromJson((String) resp.body(), GlobalToken.class).Token; - } else { - throw new RuntimeException("Error " + resp.statusCode() + ": " + (String) resp.body()); - } - } - - private String[] acquireXsts(String xblToken) throws IOException, URISyntaxException { - URI uri = new URI(xstsAuthUrl); - - Map dataProp = new ArrayMap<>(); - dataProp.put("SandboxId", "RETAIL"); - dataProp.put("UserTokens", Arrays.asList(xblToken)); - - Map data = new ArrayMap<>(); - data.put("Properties", dataProp); - data.put("RelyingParty", "rp://api.minecraftservices.com/"); - data.put("TokenType", "JWT"); - - HttpRequest request = HttpRequest.newBuilder(uri) - .header("Content-Type", "application/json") - .header("Accept", "application/json") - .POST(ofJSONData(data)).build(); - - HttpResponse resp = HttpClient.newBuilder().build().sendRequest(request, HttpResponse.BodyHandlers.ofString()); - - if (resp.statusCode() >= 200 && resp.statusCode() < 300) { - String body = (String) resp.body(); - GlobalToken token = Tools.GLOBAL_GSON.fromJson(body, GlobalToken.class); - String xblXsts = token.Token; - String uhs = token.DisplayClaims.xui[0].uhs; - - return new String[]{uhs, xblXsts}; - } else { - throw new RuntimeException("Error " + resp.statusCode() + ": " + (String) resp.body()); - } - } - - private String acquireMinecraftToken(String xblUhs, String xblXsts) throws IOException, URISyntaxException { - URI uri = new URI(mcLoginUrl); - - Map data = new ArrayMap<>(); - data.put("identityToken", "XBL3.0 x=" + xblUhs + ";" + xblXsts); - - HttpRequest request = HttpRequest.newBuilder(uri) - .header("Content-Type", "application/json") - .header("Accept", "application/json") - .POST(ofJSONData(data)).build(); - - HttpResponse resp = HttpClient.newBuilder().build().sendRequest(request, HttpResponse.BodyHandlers.ofString()); - if (resp.statusCode() >= 200 && resp.statusCode() < 300) { - String body = (String) resp.body(); - return Tools.GLOBAL_GSON.fromJson(body, GlobalToken.class).access_token; - } else { - throw new RuntimeException("Error " + resp.statusCode() + ": " + (String) resp.body()); - } - } - - private MinecraftAccount checkMcProfile(String mcAccessToken) throws IOException, URISyntaxException { - URI uri = new URI(mcProfileUrl); - - HttpRequest request = HttpRequest.newBuilder(uri) - .header("Authorization", "Bearer " + mcAccessToken) - .header("Content-Type", "application/json") - .header("Accept", "application/json") - .GET().build(); - - HttpResponse resp = HttpClient.newBuilder().build().sendRequest(request, HttpResponse.BodyHandlers.ofString()); - if (resp.statusCode() >= 200 && resp.statusCode() < 300) { - String body = (String) resp.body(); - GlobalToken token = Tools.GLOBAL_GSON.fromJson(body, GlobalToken.class); - String uuid = token.id; - String uuidDashes = uuid.replaceFirst( - "(\\p{XDigit}{8})(\\p{XDigit}{4})(\\p{XDigit}{4})(\\p{XDigit}{4})(\\p{XDigit}+)", "$1-$2-$3-$4-$5" - ); - - MinecraftAccount acc = new MinecraftAccount(); - acc.isMicrosoft = true; - acc.username = token.name; - acc.accessToken = mcAccessToken; - acc.profileId = uuidDashes; - return acc; - } else { - throw new RuntimeException("Error " + resp.statusCode() + ": " + (String) resp.body()); - } - } - - public static HttpRequest.BodyPublisher ofJSONData(Map data) { - return HttpRequest.BodyPublishers.ofString(Tools.GLOBAL_GSON.toJson(data)); - } - - public static HttpRequest.BodyPublisher ofFormData(Map data) { - StringBuilder builder = new StringBuilder(); - for (Map.Entry entry : data.entrySet()) { - if (builder.length() > 0) { - builder.append("&"); - } - try { - builder.append(URLEncoder.encode(entry.getKey().toString(), "UTF-8")); - builder.append("="); - builder.append(URLEncoder.encode(entry.getValue().toString(), "UTF-8")); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); - } - } - return HttpRequest.BodyPublishers.ofString(builder.toString()); - } - - */ -} - diff --git a/JavaApp/src/main/java/net/kdt/pojavlaunch/authenticator/microsoft/Msa.java.todo b/JavaApp/src/main/java/net/kdt/pojavlaunch/authenticator/microsoft/Msa.java similarity index 100% rename from JavaApp/src/main/java/net/kdt/pojavlaunch/authenticator/microsoft/Msa.java.todo rename to JavaApp/src/main/java/net/kdt/pojavlaunch/authenticator/microsoft/Msa.java diff --git a/JavaApp/src/main/java/net/kdt/pojavlaunch/authenticator/mojang/yggdrasil/YggdrasilAuthenticator.java b/JavaApp/src/main/java/net/kdt/pojavlaunch/authenticator/mojang/yggdrasil/YggdrasilAuthenticator.java index bdc0d1ee..ba7a1869 100644 --- a/JavaApp/src/main/java/net/kdt/pojavlaunch/authenticator/mojang/yggdrasil/YggdrasilAuthenticator.java +++ b/JavaApp/src/main/java/net/kdt/pojavlaunch/authenticator/mojang/yggdrasil/YggdrasilAuthenticator.java @@ -23,7 +23,7 @@ public class YggdrasilAuthenticator { try { HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestProperty("User-Agent", "Minecraft"); - // conn.setDoInput(true); + conn.setDoInput(true); conn.setDoOutput(true); conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", "application/json"); diff --git a/JavaApp/src/main/java/net/kdt/pojavlaunch/uikit/AccountJNI.java b/JavaApp/src/main/java/net/kdt/pojavlaunch/uikit/AccountJNI.java index 8fbb1559..25d390ce 100644 --- a/JavaApp/src/main/java/net/kdt/pojavlaunch/uikit/AccountJNI.java +++ b/JavaApp/src/main/java/net/kdt/pojavlaunch/uikit/AccountJNI.java @@ -22,15 +22,16 @@ public class AccountJNI { switch (type) { case TYPE_SELECTACC: CURRENT_ACCOUNT = MinecraftAccount.load(username); - if (CURRENT_ACCOUNT.accessToken.length() > 5) { + if (CURRENT_ACCOUNT.isMicrosoft) { + CURRENT_ACCOUNT = new MicrosoftAuthTask().run("true", CURRENT_ACCOUNT.msaRefreshToken); + } else if (CURRENT_ACCOUNT.accessToken.length() > 5) { PojavProfile.updateTokens(username); } CURRENT_ACCOUNT = MinecraftAccount.load(username); break; case TYPE_MICROSOFT: - // TODO - throw new UnsupportedOperationException("TODO"); + CURRENT_ACCOUNT = new MicrosoftAuthTask().run("false", password); // break; case TYPE_MOJANG: diff --git a/Natives/AppDelegate.m b/Natives/AppDelegate.m index 2d22fd7a..a4041080 100644 --- a/Natives/AppDelegate.m +++ b/Natives/AppDelegate.m @@ -34,4 +34,10 @@ // Use this method to release any resources that were specific to the discarded scenes, as they will not return. } +- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation { + NSDictionary *data = [NSDictionary dictionaryWithObject:url forKey:@"url"]; + [[NSNotificationCenter defaultCenter] postNotificationName:@"MSALoginCallback" object:self userInfo:data]; + return YES; +} + @end diff --git a/Natives/CMakeLists.txt b/Natives/CMakeLists.txt index 58c94838..92d4070d 100644 --- a/Natives/CMakeLists.txt +++ b/Natives/CMakeLists.txt @@ -19,4 +19,5 @@ target_link_libraries(pojavexec "-framework UIKit" "-framework MetalANGLE" "-framework CoreGraphics" + "-framework SafariServices" ) diff --git a/Natives/Info.plist b/Natives/Info.plist index 09ca300d..46a81a03 100644 --- a/Natives/Info.plist +++ b/Natives/Info.plist @@ -64,6 +64,17 @@ + CFBundleURLTypes + + + CFBundleURLName + net.kdt.pojavlaunch + CFBundleURLSchemes + + ms-xal-00000000402b5328 + + + LSRequiresIPhoneOS MinimumOSVersion diff --git a/Natives/LoginViewController.m b/Natives/LoginViewController.m index f4c77013..1e516e4b 100644 --- a/Natives/LoginViewController.m +++ b/Natives/LoginViewController.m @@ -1,6 +1,9 @@ #include #include +#import + +#import "AppDelegate.h" #import "LauncherViewController.h" #import "LoginViewController.h" @@ -10,7 +13,7 @@ #define TYPE_MICROSOFT 1 #define TYPE_MOJANG 2 #define TYPE_OFFLINE 3 - + void loginAccountInput(UINavigationController *controller, int type, char* username_c, char* password_c) { JNIEnv *env; (*runtimeJavaVMPtr)->AttachCurrentThread(runtimeJavaVMPtr, &env, NULL); @@ -36,7 +39,7 @@ void loginAccountInput(UINavigationController *controller, int type, char* usern } } -@interface LoginViewController () { +@interface LoginViewController (){ } @end @@ -48,6 +51,8 @@ void loginAccountInput(UINavigationController *controller, int type, char* usern [self setTitle:@"PojavLauncher"]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(msaLoginCallback:) name:@"MSALoginCallback" object:nil]; + UIBarButtonItem *backItem = [[UIBarButtonItem alloc] initWithTitle:@"Back" style:UIBarButtonItemStylePlain target:nil action:nil]; [self.navigationItem setBackBarButtonItem:backItem]; @@ -167,7 +172,10 @@ void loginAccountInput(UINavigationController *controller, int type, char* usern } - (void)loginMicrosoft { - + NSURL *url = [NSURL URLWithString:@"https://login.live.com/oauth20_authorize.srf?client_id=00000000402b5328&response_type=code&scope=service%3A%3Auser.auth.xboxlive.com%3A%3AMBI_SSL&redirect_url=https%3A%2F%2Flogin.live.com%2Foauth20_desktop.srf"]; + SFSafariViewController *vc = [[SFSafariViewController alloc] initWithURL:url]; + vc.delegate = self; + [self presentViewController:vc animated:YES completion:nil]; } - (void)loginOffline { @@ -179,6 +187,20 @@ void loginAccountInput(UINavigationController *controller, int type, char* usern [self.navigationController pushViewController:vc animated:YES]; } +- (void)safariViewControllerDidFinish:(SFSafariViewController *)controller { + [controller dismissViewControllerAnimated:true completion:nil]; +} + +- (void)msaLoginCallback:(NSNotification *)notification { + [self dismissViewControllerAnimated:YES completion:nil]; + + NSDictionary *dict = [notification userInfo]; + NSURL *url = [dict objectForKey:@"url"]; + NSArray *components = [[url absoluteString] componentsSeparatedByString:@"?code="]; + // NSLog(@"URL returned = %@", components[1]); + loginAccountInput(self.navigationController, TYPE_MICROSOFT, "", [components[1] UTF8String]); +} + @end @interface LoginListViewController () { diff --git a/Natives/SceneDelegate.m b/Natives/SceneDelegate.m index 41329adb..2f62e2be 100644 --- a/Natives/SceneDelegate.m +++ b/Natives/SceneDelegate.m @@ -57,5 +57,12 @@ // to restore the scene back to its current state. } +- (void)scene:(UIScene *)scene openURLContexts:(nonnull NSSet *)URLContexts +API_AVAILABLE(ios(13.0)){ + NSURL *url = [[URLContexts allObjects] firstObject].URL; + NSDictionary *data = [NSDictionary dictionaryWithObject:url forKey:@"url"]; + [[NSNotificationCenter defaultCenter] postNotificationName:@"MSALoginCallback" object:self userInfo:data]; + +} @end diff --git a/Natives/aminimake.sh b/Natives/aminimake.sh index ea1872e6..9d912af9 100644 --- a/Natives/aminimake.sh +++ b/Natives/aminimake.sh @@ -5,7 +5,7 @@ set -e CFLAGS="-fmessage-length=0 -fdiagnostics-show-note-include-stack -fmacro-backtrace-limit=0 -Wno-trigraphs -fpascal-strings -O3 -Wno-missing-field-initializers -Wno-missing-prototypes -Wno-return-type -Wno-missing-braces -Wparentheses -Wswitch -Wno-unused-function -Wno-unused-label -Wno-unused-parameter -Wno-unused-variable -Wunused-value -Wno-empty-body -Wno-uninitialized -Wno-unknown-pragmas -Wno-shadow -Wno-four-char-constants -Wno-conversion -Wno-constant-conversion -Wno-int-conversion -Wno-bool-conversion -Wno-enum-conversion -Wno-float-conversion -Wno-non-literal-null-conversion -Wno-objc-literal-conversion -Wshorten-64-to-32 -Wpointer-sign -Wno-newline-eof" -clang -Wl,-rpath,/Applications/PojavLauncher.app/Frameworks -fobjc-arc -x objective-c -Fresources/Frameworks -framework UIKit -framework MetalANGLE -framework CoreGraphics -Iresources/Frameworks/MetalANGLE.framework/Headers -dynamiclib -isysroot /var/mobile/theos/sdks/iPhoneOS13.4.sdk $CFLAGS -o libpojavexec.dylib log.m AppDelegate.m SceneDelegate.m UILauncher.m LauncherViewController.m LoginViewController.m SurfaceViewController.m egl_bridge_ios.m ios_uikit_bridge.m egl_bridge.c input_bridge_v3.c utils.c -DGLES_TESTZ -DUSE_EGLZ +clang -Wl,-rpath,/Applications/PojavLauncher.app/Frameworks -fobjc-arc -x objective-c -Fresources/Frameworks -framework UIKit -framework MetalANGLE -framework CoreGraphics -framework SafariServices -Iresources/Frameworks/MetalANGLE.framework/Headers -dynamiclib -isysroot /var/mobile/theos/sdks/iPhoneOS13.4.sdk $CFLAGS -o libpojavexec.dylib log.m AppDelegate.m SceneDelegate.m UILauncher.m LauncherViewController.m LoginViewController.m SurfaceViewController.m egl_bridge_ios.m ios_uikit_bridge.m egl_bridge.c input_bridge_v3.c utils.c -DGLES_TESTZ -DUSE_EGLZ ldid -S../entitlements.xml libpojavexec.dylib