diff --git a/.github/workflows/compile.yml b/.github/workflows/compile.yml index 4a0c1f9..300a7b4 100644 --- a/.github/workflows/compile.yml +++ b/.github/workflows/compile.yml @@ -40,7 +40,7 @@ jobs: compile: name: Compile the stub needs: draft_release # we need to know the upload URL - runs-on: macos-11 + runs-on: macos-latest steps: - uses: actions/checkout@v2 @@ -73,6 +73,10 @@ jobs: strip universalJavaApplicationStub chmod ug=rwx,o=rx universalJavaApplicationStub + - name: Build nativeJavaApplicationStub + run: | + make universal + - name: Upload universalJavaApplicationStub.x86_64 asset uses: actions/upload-release-asset@v1 env: @@ -103,6 +107,36 @@ jobs: asset_path: ./universalJavaApplicationStub asset_content_type: application/octet-stream + - name: Upload nativeJavaApplicationStub.x86_64 asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.draft_release.outputs.upload_url }} + asset_name: nativeJavaApplicationStub.x86_64 + asset_path: ./build/x86_64/nativeJavaApplicationStub + asset_content_type: application/octet-stream + + - name: Upload nativeJavaApplicationStub.arm64 asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.draft_release.outputs.upload_url }} + asset_name: nativeJavaApplicationStub.arm64 + asset_path: ./build/arm64/nativeJavaApplicationStub + asset_content_type: application/octet-stream + + - name: Upload nativeJavaApplicationStub asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.draft_release.outputs.upload_url }} + asset_name: nativeJavaApplicationStub + asset_path: ./build/universal/nativeJavaApplicationStub + asset_content_type: application/octet-stream + publish_release: name: Publish drafted release needs: [ draft_release, compile ] diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..49cc67f --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,64 @@ +name: Build on push + +on: push + +jobs: + compile: + name: Compile the stubs + runs-on: macos-latest + steps: + - uses: actions/checkout@v2 + + - name: Install shc via HomeBrew + run: | + brew install shc + shc -h + + - name: Compile universalJavaApplicationStub + run: | + echo "Running shc..." + shc -r -f src/universalJavaApplicationStub + + - name: Build universalJavaApplicationStub x86_64 + run: | + echo "Running clang for universalJavaApplicationStub.x86_64" + clang -o universalJavaApplicationStub.x86_64 -target x86_64-apple-macos10.10 src/universalJavaApplicationStub.x.c + strip universalJavaApplicationStub.x86_64 + + - name: Build universalJavaApplicationStub arm64 + run: | + echo "Running clang for universalJavaApplicationStub.arm64" + clang -o universalJavaApplicationStub.arm64 -target arm64-apple-macos11 src/universalJavaApplicationStub.x.c + strip universalJavaApplicationStub.arm64 + + - name: Build universal universalJavaApplicationStub + run: | + echo "Runnning lipo" + lipo -create -output universalJavaApplicationStub universalJavaApplicationStub.x86_64 universalJavaApplicationStub.arm64 + strip universalJavaApplicationStub + chmod ug=rwx,o=rx universalJavaApplicationStub + + - name: Build nativeJavaApplicationStub + run: | + make universal + + - name: Archive production artifacts + uses: actions/upload-artifact@v4 + with: + name: universalJavaApplicationStub + retention-days: 7 + path: | + ./src/universalJavaApplicationStub + ./universalJavaApplicationStub.x86_64 + ./universalJavaApplicationStub.arm64 + ./universalJavaApplicationStub + + - name: Archive production artifacts + uses: actions/upload-artifact@v4 + with: + name: nativeJavaApplicationStub + retention-days: 7 + path: | + ./build/x86_64/nativeJavaApplicationStub + ./build/arm64/nativeJavaApplicationStub + ./build/universal/nativeJavaApplicationStub diff --git a/.gitignore b/.gitignore index 6053715..f575bcb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ # shc compiler *.x -*.x.c \ No newline at end of file +*.x.c + +build \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3112f9f --- /dev/null +++ b/Makefile @@ -0,0 +1,57 @@ +CC = clang + +CFLAGS = -Wall -Werror -g +CFLAGS_ARM64 = -target arm64-apple-macos11 +CFLAGS_X86_64 = -target x86_64-apple-macos10.12 +LIBS = -lobjc -framework Foundation -framework AppKit +INCLUDES = + +SRCS = src/nativeStub.m +OBJS = $(SRCS:src/%.m=build/%.o) +OBJS_ARM64 = $(SRCS:src/%.m=build/arm64/%.o) +OBJS_X86_64 = $(SRCS:src/%.m=build/x86_64/%.o) + +# define the executable file +APP_NAME = nativeJavaApplicationStub +MAIN = build/$(APP_NAME) +MAIN_ARM64 = build/arm64/$(APP_NAME) +MAIN_X86_64 = build/x86_64/$(APP_NAME) +MAIN_UNIVERSAL = build/universal/$(APP_NAME) + +.PHONY: depend clean + +default: $(MAIN) +universal: $(MAIN_UNIVERSAL) + +$(MAIN): $(OBJS) + $(CC) $(CFLAGS) $(INCLUDES) -o $@ $^ $(LFLAGS) $(LIBS) + +$(MAIN_ARM64): $(OBJS_ARM64) + $(CC) $(CFLAGS) $(CFLAGS_ARM64) $(INCLUDES) -o $@ $^ $(LFLAGS) $(LIBS) + +$(MAIN_X86_64): $(OBJS_X86_64) + $(CC) $(CFLAGS) $(CFLAGS_X86_64) $(INCLUDES) -o $@ $^ $(LFLAGS) $(LIBS) + +$(MAIN_UNIVERSAL): $(MAIN_ARM64) $(MAIN_X86_64) + @mkdir -p build/universal + lipo -create -output $@ $^ + +build/%.o: src/%.m + @mkdir -p build + $(CC) $(CFLAGS) $(INCLUDES) -c $< -o $@ + +build/arm64/%.o: src/%.m + @mkdir -p build/arm64 + $(CC) $(CFLAGS) $(INCLUDES) $(CFLAGS_ARM64) -c $< -o $@ + +build/x86_64/%.o: src/%.m + @mkdir -p build/x86_64 + $(CC) $(CFLAGS) $(INCLUDES) $(CFLAGS_X86_64) -c $< -o $@ + +clean: + rm -rf build/ + +depend: $(SRCS) + makedepend $(INCLUDES) $^ + +# DO NOT DELETE THIS LINE -- make depend needs it \ No newline at end of file diff --git a/src/nativeStub.h b/src/nativeStub.h new file mode 100644 index 0000000..87fd855 --- /dev/null +++ b/src/nativeStub.h @@ -0,0 +1,15 @@ +#import + +@interface JVMMetadata : NSObject +@property(strong) NSString *path; +@property(strong) NSString *version; +@end + +NSString *resolvePlaceholders(NSString *src, NSString *javaFolder); +NSString *execute(NSString *command, NSArray *args); +NSString *fetchJavaVersion(NSString *path); +NSString *normalizeJavaVersion(NSString *version); +BOOL isValidRequirement(NSString *version); +BOOL versionMeetsConstraint(NSString *version, NSString *constraint, BOOL hasMax); +BOOL versionMeetsMaxConstraint(NSString *version, NSString *constraint); + diff --git a/src/nativeStub.m b/src/nativeStub.m new file mode 100644 index 0000000..497f007 --- /dev/null +++ b/src/nativeStub.m @@ -0,0 +1,524 @@ +#import "nativeStub.h" +#import +#import +#import + +int main(int argc, char** argv) { + // For future improvement we can make these localized strings actually have the translations + // like they do in the bash script + NSString *MSG_ERROR_LAUNCHING=NSLocalizedStringWithDefaultValue(@"MSG_ERROR_LAUNCHING", @"javaApplicationStub", NSBundle.mainBundle, @"ERROR launching '%s'.", nil); + NSString *MSG_MISSING_MAINCLASS=NSLocalizedStringWithDefaultValue(@"MSG_MISSING_MAINCLASS", @"javaApplicationStub", NSBundle.mainBundle, @"'MainClass' isn't specified!\nJava application cannot be started!", nil); + NSString *MSG_JVMVERSION_REQ_INVALID=NSLocalizedStringWithDefaultValue(@"MSG_JVMVERSION_REQ_INVALID", @"javaApplicationStub", NSBundle.mainBundle, @"The syntax of the required Java version is invalid: %@\nPlease contact the App developer.", nil); + NSString *MSG_NO_SUITABLE_JAVA=NSLocalizedStringWithDefaultValue(@"MSG_NO_SUITABLE_JAVA", @"javaApplicationStub", NSBundle.mainBundle, @"No suitable Java version found on your system!\nThis program requires Java %@", nil); + NSString *MSG_JAVA_VERSION_OR_LATER=NSLocalizedStringWithDefaultValue(@"MSG_JAVA_VERSION_OR_LATER", @"javaApplicationStub", NSBundle.mainBundle, @" or later", nil); + NSString *MSG_JAVA_VERSION_LATEST=NSLocalizedStringWithDefaultValue(@"MSG_JAVA_VERSION_LATEST", @"javaApplicationStub", NSBundle.mainBundle, @" (latest update)", nil); + NSString *MSG_JAVA_VERSION_MAX=NSLocalizedStringWithDefaultValue(@"MSG_JAVA_VERSION_MAX", @"javaApplicationStub", NSBundle.mainBundle, @"up to %@", nil); + NSString *MSG_NO_SUITABLE_JAVA_CHECK=NSLocalizedStringWithDefaultValue(@"MSG_NO_SUITABLE_JAVA_CHECK", @"javaApplicationStub", NSBundle.mainBundle, @"Make sure you install the required Java version.", nil); + NSString *MSG_INSTALL_JAVA=NSLocalizedStringWithDefaultValue(@"MSG_INSTALL_JAVA", @"javaApplicationStub", NSBundle.mainBundle, @"You need to have JAVA installed on your Mac!\nVisit java.com for installation instructions...", nil); + NSString *MSG_LATER=NSLocalizedStringWithDefaultValue(@"MSG_LATER", @"javaApplicationStub", NSBundle.mainBundle, @"Later", nil); + NSString *MSG_VISIT_JAVA_DOT_COM=NSLocalizedStringWithDefaultValue(@"MSG_VISIT_JAVA_DOT_COM", @"javaApplicationStub", NSBundle.mainBundle, @"Java by Oracle", nil); + NSString *MSG_VISIT_ADOPTIUM=NSLocalizedStringWithDefaultValue(@"MSG_VISIT_ADOPTIUM", @"javaApplicationStub", NSBundle.mainBundle, @"Java by Adoptium", nil); + + NSBundle *main = [NSBundle mainBundle]; + NSDictionary *info = [main infoDictionary]; + const char *appName = [info[@"CFBundleName"] UTF8String]; + NSLog(@"[%s] [StubPath] %@", appName, [main executablePath]); + + NSString *iconFile = info[@"CFBundleIconFile"]; + if(iconFile != nil && ![iconFile containsString:@".icns"]) { + iconFile = [iconFile stringByAppendingString:@".icns"]; + } + NSDictionary *javaInfo = info[@"Java"]; + if(javaInfo == nil) { + javaInfo = info[@"JavaX"]; + } + + NSString *javaFolder; + NSString *mainClass; + NSString *splashFile; + NSString *workingDirectory; + NSMutableArray *jvmOptions; + NSMutableArray *jvmDefaultOptions; + NSMutableArray *classPath; + NSMutableArray *mainArgs; + NSString *jvmVersion; + NSString *jvmMaxVersion = nil; + NSString *jvmOptionsFile; + NSString *bootstrapScript; + if(javaInfo != nil) { + NSLog(@"[%s] [PlistStyle] Apple", appName); + // Apple mode + javaFolder = [[main resourcePath] stringByAppendingPathComponent:@"Java"]; + if(javaInfo[@"RelocateJar"]) { + javaFolder = [main resourcePath]; + } + + if(javaInfo[@"WorkingDirectory"] != nil) { + NSString *workDirWithPlaceholders = javaInfo[@"WorkingDirectory"]; + workingDirectory = resolvePlaceholders(workDirWithPlaceholders, javaFolder); + } else { + workingDirectory = [[main bundlePath] stringByDeletingLastPathComponent]; + } + + mainClass = javaInfo[@"MainClass"]; + splashFile = javaInfo[@"SplashFile"]; + jvmOptions = [[NSMutableArray alloc] init]; + NSDictionary *propertiesAttr = javaInfo[@"Properties"]; + if(propertiesAttr != nil) { + for(NSString *key in propertiesAttr) { + [jvmOptions addObject:[NSString stringWithFormat:@"-D%@=%@", resolvePlaceholders(key, javaFolder), resolvePlaceholders(propertiesAttr[key], javaFolder)]]; + } + } + + classPath = [[NSMutableArray alloc] init]; + id classPathAttr = javaInfo[@"ClassPath"]; + if([classPathAttr isKindOfClass:[NSArray class]]) { + for(NSString *pathElement in classPathAttr) { + [classPath addObject:resolvePlaceholders(pathElement, javaFolder)]; + } + } else if(classPathAttr != nil) { + [classPath addObject:resolvePlaceholders(classPathAttr, javaFolder)]; + } + + jvmDefaultOptions = [[NSMutableArray alloc] init]; + id vmOptionsAttr = javaInfo[@"VMOptions"]; + if([vmOptionsAttr isKindOfClass:[NSArray class]]) { + for(NSString *pathElement in vmOptionsAttr) { + [jvmDefaultOptions addObject:resolvePlaceholders(pathElement, javaFolder)]; + } + } else if(vmOptionsAttr != nil) { + [jvmDefaultOptions addObject:resolvePlaceholders(vmOptionsAttr, javaFolder)]; + } + + if(javaInfo[@"StartOnMainThread"]) { + [jvmDefaultOptions addObject:@" -XstartOnFirstThread"]; + } + + mainArgs = [[NSMutableArray alloc] init]; + id argumentsAttr = javaInfo[@"Arguments"]; + if([argumentsAttr isKindOfClass:[NSArray class]]) { + for(NSString *pathElement in argumentsAttr) { + [mainArgs addObject:resolvePlaceholders(pathElement, javaFolder)]; + } + } else if(argumentsAttr != nil) { + [mainArgs addObject:resolvePlaceholders(argumentsAttr, javaFolder)]; + } + + jvmVersion = javaInfo[@"JVMVersion"]; + jvmOptionsFile = javaInfo[@"JVMOptionsFile"]; + bootstrapScript = javaInfo[@"BootstrapScript"]; + } else { + NSLog(@"[%s] [PlistStyle] Oracle", appName); + javaFolder = [[main bundlePath] stringByAppendingPathComponent:@"Contents/Java"]; + workingDirectory = javaFolder; + mainClass = info[@"JVMMainClassName"]; + mainClass = info[@"JVMSplashFile"]; + + jvmOptions = [[NSMutableArray alloc] init]; + NSDictionary *propertiesAttr = info[@"JVMOptions"]; + if(propertiesAttr != nil) { + for(NSString *key in propertiesAttr) { + [jvmOptions addObject:[NSString stringWithFormat:@"-D%@=%@", resolvePlaceholders(key, javaFolder), resolvePlaceholders(propertiesAttr[key], javaFolder)]]; + } + } + + classPath = [[NSMutableArray alloc] init]; + id classPathAttr = info[@"JVMClassPath"]; + if([classPathAttr isKindOfClass:[NSArray class]]) { + for(NSString *pathElement in classPathAttr) { + [classPath addObject:resolvePlaceholders(pathElement, javaFolder)]; + } + } else if(classPathAttr != nil) { + [classPath addObject:resolvePlaceholders(classPathAttr, javaFolder)]; + } else { + [classPath addObject:[javaFolder stringByAppendingString:@"/*"]]; + } + + jvmDefaultOptions = [[NSMutableArray alloc] init]; + id vmOptionsAttr = info[@"JVMDefaultOptions"]; + for(NSString *pathElement in vmOptionsAttr) { + [jvmDefaultOptions addObject:resolvePlaceholders(pathElement, javaFolder)]; + } + + mainArgs = [[NSMutableArray alloc] init]; + id argumentsAttr = info[@"JVMArguments"]; + if([argumentsAttr isKindOfClass:[NSArray class]]) { + for(NSString *pathElement in argumentsAttr) { + [mainArgs addObject:resolvePlaceholders(pathElement, javaFolder)]; + } + } else if(argumentsAttr != nil) { + [mainArgs addObject:resolvePlaceholders(argumentsAttr, javaFolder)]; + } + + jvmVersion = info[@"JVMVersion"]; + jvmOptionsFile = info[@"JVMOptionsFile"]; + bootstrapScript = info[@"BootstrapScript"]; + } + if([jvmVersion containsString:@";"]) { + NSArray *stringParts = [jvmVersion componentsSeparatedByString:@";"]; + jvmVersion = stringParts[0]; + jvmMaxVersion = stringParts[1]; + } + NSLog(@"[%s] [JavaRequirement] JVM minimum version: %@", appName, jvmVersion); + NSLog(@"[%s] [JavaRequirement] JVM minimum version: %@", appName, jvmMaxVersion); + + if(jvmVersion != nil && !isValidRequirement(jvmVersion)) { + NSString *errorMsg = [NSString stringWithFormat:MSG_JVMVERSION_REQ_INVALID, jvmVersion]; + NSLog(@"[%s] [EXIT 4] %@", appName, errorMsg); + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:[NSString stringWithFormat:MSG_ERROR_LAUNCHING, appName]]; + [alert setInformativeText:errorMsg]; + [alert runModal]; + exit(4); + } + if(jvmVersion != nil && !isValidRequirement(jvmMaxVersion)) { + NSString *errorMsg = [NSString stringWithFormat:MSG_JVMVERSION_REQ_INVALID, jvmMaxVersion]; + NSLog(@"[%s] [EXIT 5] %@", appName, MSG_MISSING_MAINCLASS); + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:[NSString stringWithFormat:MSG_ERROR_LAUNCHING, appName]]; + [alert setInformativeText:errorMsg]; + [alert runModal]; + exit(5); + } + + NSLog(@"[%s] [JavaSearch] Checking for $JAVA_HOME ...", appName); + NSString *javaHome = [[[NSProcessInfo processInfo] environment] objectForKey:@"JAVA_HOME"]; + NSString *javaCmd = nil; + if(javaHome != nil) { + NSLog(@"[%s] [JavaSearch] ... found JAVA_HOME with value %@", appName, javaHome); + if([javaHome characterAtIndex:0] == '/') { + javaCmd = [javaHome stringByAppendingString:@"/bin/java"]; + NSLog(@"[%s] [JavaSearch] ... parsing JAVA_HOME as absolute path to the executable '%@'", appName, javaCmd); + } else { + javaCmd = [[main bundlePath] stringByAppendingString:[@"/" stringByAppendingString:[javaHome stringByAppendingString:@"/bin/java"]]]; + NSLog(@"[%s] [JavaSearch] ... parsing JAVA_HOME as relative path inside the App bundle to the executable '%@'", appName, javaCmd); + } + } else { + NSLog(@"[%s] [JavaSearch] ... haven't found JAVA_HOME", appName); + } + + NSFileManager *fileManager = [NSFileManager defaultManager]; + if (javaCmd == nil || ![fileManager isExecutableFileAtPath:javaCmd]) { + if(javaCmd != nil) { + NSLog(@"[%s] [JavaSearch] ... but no 'java' executable was found at the JAVA_HOME location!", appName); + javaCmd = nil; + } + + NSLog(@"[%s] [JavaSearch] Searching for JavaVirtualMachines on the system ...", appName); + + NSMutableArray *allJvms = [[NSMutableArray alloc] init]; + + NSString *javaHomeRaw = execute(@"/usr/libexec/java_home", @[@"--xml"]); + NSData* plistData = [javaHomeRaw dataUsingEncoding:NSUTF8StringEncoding]; + NSArray* plist = [NSPropertyListSerialization propertyListWithData:plistData options:NSPropertyListImmutable format:nil error:nil]; + if(plist != nil) { + for(NSDictionary *entry in plist) { + JVMMetadata *metadata = [[JVMMetadata alloc] init]; + [metadata setPath:[entry[@"JVMHomePath"] stringByAppendingString:@"/bin/java"]]; + [metadata setVersion:normalizeJavaVersion(entry[@"JVMVersion"])]; + [allJvms addObject:metadata]; + } + } + + if([fileManager isExecutableFileAtPath:@"/Library/Java/Home/bin/java"]) { + JVMMetadata *metadata = [[JVMMetadata alloc] init]; + [metadata setPath:@"/Library/Java/Home/bin/java"]; + [metadata setVersion:normalizeJavaVersion(fetchJavaVersion([metadata path]))]; + [allJvms addObject:metadata]; + } + + if([fileManager isExecutableFileAtPath:@"/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java"]) { + JVMMetadata *metadata = [[JVMMetadata alloc] init]; + [metadata setPath:@"/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java"]; + [metadata setVersion:normalizeJavaVersion(fetchJavaVersion([metadata path]))]; + [allJvms addObject:metadata]; + } + + BOOL isDir = NO; + NSString *searchBase = [@"~/.sdkman/candidates/java" stringByExpandingTildeInPath]; + if([fileManager fileExistsAtPath:searchBase isDirectory:&isDir] && isDir) { + for(NSString *path in [fileManager contentsOfDirectoryAtPath:searchBase error:nil]) { + if([path isEqualToString:@"current/"]) { + continue; + } + JVMMetadata *metadata = [[JVMMetadata alloc] init]; + [metadata setPath:[NSString stringWithFormat:@"%@/%@/bin/java", searchBase, path]]; + if([fileManager isExecutableFileAtPath:[metadata path]]) { + [metadata setVersion:normalizeJavaVersion(fetchJavaVersion([metadata path]))]; + [allJvms addObject:metadata]; + } + } + } + + searchBase = [@"~/.asdf/installs/java" stringByExpandingTildeInPath]; + if([fileManager fileExistsAtPath:searchBase isDirectory:&isDir] && isDir) { + for(NSString *path in [fileManager contentsOfDirectoryAtPath:searchBase error:nil]) { + JVMMetadata *metadata = [[JVMMetadata alloc] init]; + [metadata setPath:[NSString stringWithFormat:@"%@/%@/bin/java", searchBase, path]]; + if([fileManager isExecutableFileAtPath:[metadata path]]) { + [metadata setVersion:normalizeJavaVersion(fetchJavaVersion([metadata path]))]; + [allJvms addObject:metadata]; + } + } + } + + for(JVMMetadata *metadata in allJvms) { + NSLog(@"[%s] [JavaSearch] ... found JVM: %@", appName, metadata); + } + + NSLog(@"[%s] [JavaSearch] Filtering the result list for JVMs matching the min/max version requirement ...", appName); + + NSMutableArray *matchingJvms = [[NSMutableArray alloc] init]; + for(JVMMetadata *metadata in allJvms) { + if(jvmVersion != nil && !versionMeetsConstraint([metadata version], normalizeJavaVersion(jvmVersion), jvmMaxVersion != nil)) { + continue; + } + if(jvmMaxVersion != nil && !versionMeetsMaxConstraint([metadata version], normalizeJavaVersion(jvmMaxVersion))) { + continue; + } + [matchingJvms addObject:metadata]; + } + + for(JVMMetadata *metadata in matchingJvms) { + NSLog(@"[%s] [JavaSearch] ... matches all requirements: %@", appName, metadata); + } + NSArray *sortedMatchingJvms = [matchingJvms sortedArrayUsingComparator:^NSComparisonResult(JVMMetadata *obj1, JVMMetadata *obj2) { + return [[obj2 version] compare:[obj1 version] options:NSNumericSearch]; + }]; + for(JVMMetadata *metadata in sortedMatchingJvms) { + if([fileManager isExecutableFileAtPath: [metadata path]]) { + javaCmd = [metadata path]; + break; + } + } + } + + NSLog(@"[%s] [JavaCommand] '%@'", appName, javaCmd); + NSLog(@"[%s] [JavaVersion] %@", appName, javaCmd == nil ? nil : fetchJavaVersion(javaCmd)); + + if(javaCmd == nil || ![fileManager isExecutableFileAtPath:javaCmd]) { + if(jvmVersion != nil) { + NSString *expandedMessage; + if(jvmMaxVersion == nil) { + expandedMessage = [NSString stringWithFormat:MSG_NO_SUITABLE_JAVA, [ + [normalizeJavaVersion(jvmVersion) stringByReplacingOccurrencesOfString:@"*" withString: MSG_JAVA_VERSION_LATEST] + stringByReplacingOccurrencesOfString:@"+" withString: MSG_JAVA_VERSION_OR_LATER]]; + } else { + expandedMessage = [NSString stringWithFormat:@"%@ %@", + [NSString stringWithFormat:MSG_NO_SUITABLE_JAVA, normalizeJavaVersion(jvmVersion)], + [NSString stringWithFormat:MSG_JAVA_VERSION_MAX, [normalizeJavaVersion(jvmMaxVersion) stringByReplacingOccurrencesOfString:@"*" withString: MSG_JAVA_VERSION_LATEST]]]; + } + NSLog(@"[%s] [EXIT 3] %@", appName, expandedMessage); + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:[NSString stringWithFormat:MSG_ERROR_LAUNCHING, appName]]; + [alert setInformativeText:[NSString stringWithFormat:@"%@\n%@", expandedMessage, MSG_NO_SUITABLE_JAVA_CHECK]]; + [alert addButtonWithTitle:MSG_LATER]; + [alert addButtonWithTitle:MSG_VISIT_JAVA_DOT_COM]; + [alert addButtonWithTitle:MSG_VISIT_ADOPTIUM]; + NSModalResponse res = [alert runModal]; + if(res == NSAlertSecondButtonReturn) { + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://www.java.com/download/"]]; + } else if(res == NSAlertThirdButtonReturn) { + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://adoptium.net/temurin/releases/"]]; + } + exit(3); + } else { + NSLog(@"[%s] [EXIT 1] %@", appName, MSG_INSTALL_JAVA); + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:[NSString stringWithFormat:MSG_ERROR_LAUNCHING, appName]]; + [alert setInformativeText:MSG_INSTALL_JAVA]; + [alert addButtonWithTitle:MSG_LATER]; + [alert addButtonWithTitle:MSG_VISIT_JAVA_DOT_COM]; + [alert addButtonWithTitle:MSG_VISIT_ADOPTIUM]; + NSModalResponse res = [alert runModal]; + if(res == NSAlertSecondButtonReturn) { + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://www.java.com/download/"]]; + } else if(res == NSAlertThirdButtonReturn) { + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://adoptium.net/temurin/releases/"]]; + } + exit(1); + } + } + + if(mainClass == nil) { + NSLog(@"[%s] [EXIT 2] %@", appName, MSG_MISSING_MAINCLASS); + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:[NSString stringWithFormat:MSG_ERROR_LAUNCHING, appName]]; + [alert setInformativeText:MSG_MISSING_MAINCLASS]; + [alert runModal]; + exit(2); + } + + chdir([workingDirectory UTF8String]); + char cwd[PATH_MAX]; + getcwd(cwd, PATH_MAX); + NSLog(@"[%s] [WorkingDirectory] %s", appName, cwd); + if(bootstrapScript != nil && [fileManager isExecutableFileAtPath:bootstrapScript]) { + execute(bootstrapScript, @[]); + } + if(jvmOptionsFile != nil && [fileManager fileExistsAtPath:jvmOptionsFile]) { + NSString *optionsFile = [[NSString alloc] initWithData:[fileManager contentsAtPath:jvmOptionsFile] encoding:NSUTF8StringEncoding]; + NSArray *lines = [optionsFile componentsSeparatedByString:@"\n"]; + for(NSString *line in lines) { + if([line hasPrefix:@"#"]) { + continue; + } + [jvmDefaultOptions addObject:line]; + } + } + NSMutableArray *allArgs = [[NSMutableArray alloc] init]; + [allArgs addObject:javaCmd]; + [allArgs addObject:@"-cp"]; + [allArgs addObject:[classPath componentsJoinedByString:@":"]]; + if(splashFile != nil) { + [allArgs addObject:[@"-splash:" stringByAppendingFormat:@"%@/%@", [main resourcePath], splashFile]]; + } + [allArgs addObject:[@"-Xdock:icon=" stringByAppendingFormat:@"%@/%@", [main resourcePath], iconFile]]; + [allArgs addObject:[@"-Xdock:name=" stringByAppendingString:info[@"CFBundleName"]]]; + [allArgs addObjectsFromArray:jvmOptions]; + [allArgs addObjectsFromArray:jvmDefaultOptions]; + [allArgs addObject:mainClass]; + [allArgs addObjectsFromArray:mainArgs]; + for(int i = 1; i < argc; i++) { + NSString *cliArg = [[NSString alloc] initWithCString:argv[i] encoding:NSUTF8StringEncoding]; + if(i == 1 && [cliArg hasPrefix:@"-psn"]) { + break; + } + [allArgs addObject:cliArg]; + } + + int count = [allArgs count]; + char** cargs = malloc(sizeof(char*) * (count + 1)); + + for (int i = 0; i < count; ++i) { + cargs[i] = (char *) [[allArgs objectAtIndex:i] UTF8String]; + } + + cargs[count] = nil; + NSLog(@"[%s] [Exec] %@", appName, [allArgs componentsJoinedByString:@" "]); + + execv([javaCmd UTF8String], cargs); +} + +@implementation JVMMetadata +- (NSString *)description { + return [NSString stringWithFormat: @"JVMMetadata: path=%@, version=%@", _path, _version]; +} +@end + +NSString *resolvePlaceholders(NSString *src, NSString *javaFolder) { + NSBundle *main = [NSBundle mainBundle]; + + NSString *ret = src; + ret = [ret + stringByReplacingOccurrencesOfString:@"$APP_PACKAGE" + withString:[main bundlePath]]; + ret = [ret + stringByReplacingOccurrencesOfString:@"$APP_ROOT" + withString:[main bundlePath]]; + ret = [ret + stringByReplacingOccurrencesOfString:@"$JAVAROOT" + withString:javaFolder]; + ret = [ret + stringByReplacingOccurrencesOfString:@"$USER_HOME" + withString:NSHomeDirectory()]; + return ret; +} + +NSString *execute(NSString *command, NSArray *args) { + NSTask *task = [[NSTask alloc]init]; + [task setLaunchPath:command]; + [task setArguments:args]; + + NSPipe *pipe = [[NSPipe alloc]init]; + [task setStandardOutput: pipe]; + [task setStandardError: pipe]; + [task launch]; + + NSData *data = [[pipe fileHandleForReading] readDataToEndOfFile]; + NSString *output = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + + [task waitUntilExit]; + + return output; +} + +NSString *fetchJavaVersion(NSString *path) { + NSString *result = execute(path, @[@"-version"]); + // The actual version will be between the first two quotes in the result + // We can reasonably ignore all the rest of the output + return [result componentsSeparatedByString:@"\""][1]; +} + +NSString *normalizeJavaVersion(NSString *version) { + if([version hasPrefix:@"1."]) { + version = [version substringFromIndex:2]; + } + return [version stringByReplacingOccurrencesOfString:@"_" withString:@"."]; +} + +BOOL isValidRequirement(NSString *version) { + NSString *versionPatterns = @"^(1\\.[4-8](\\.[0-9]+)?(\\.0_[0-9]+)?[*+]?|[0-9]+(-ea|[*+]|(\\.[0-9]+){1,2}[*+]?)?)$"; + NSPredicate *test = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", versionPatterns]; + + return [test evaluateWithObject: version]; +} + +BOOL versionMeetsConstraint(NSString *version, NSString *constraint, BOOL hasMax) { + NSArray *versionParts = [version componentsSeparatedByString:@"."]; + NSArray *constraintParts = [constraint componentsSeparatedByString:@"."]; + BOOL exceeds = NO; + for(int i = 0; i < constraintParts.count; i++) { + int v = [versionParts[i] intValue]; + int c = [constraintParts[i] intValue]; + if(v < c) { + return NO; + } + if(v > c) { + exceeds = YES; + break; + } + } + // At this point the numeric parts are all the same or greater, so compare the suffixes + // to see which rule to apply + char constraintModifier = [constraint characterAtIndex:([constraint length] - 1)]; + // If there's a max, the bottom constraint is always effectively a min + if(constraintModifier == '+' || hasMax) { + return YES; + } else { + // no modifier is equivalent to an implicit * + return !exceeds; + } +} + +BOOL versionMeetsMaxConstraint(NSString *version, NSString *constraint) { + NSArray *versionParts = [version componentsSeparatedByString:@"."]; + NSArray *constraintParts = [constraint componentsSeparatedByString:@"."]; + BOOL exceeds = NO; + for(int i = 0; i < [constraintParts count]; i++) { + if([constraintParts[i] length] == 0) { + break; + } + int v = [versionParts[i] intValue]; + int c = [constraintParts[i] intValue]; + if(v < c) { + return YES; + } + if(v > c) { + exceeds = YES; + break; + } + } + + // At this point the numeric parts are all the same or greater, so compare the suffixes + // to see which rule to apply + char constraintModifier = [constraint characterAtIndex:([constraint length] - 1)]; + if(constraintModifier == '+') { + return YES; + } else if(constraintModifier == '*') { + return !exceeds; + } else { + // no modifier means it must match exactly + return !exceeds && [constraintParts count] == [versionParts count]; + } +} \ No newline at end of file