diff --git a/cookiecutter.json b/cookiecutter.json index 1891354..b738b0c 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -12,6 +12,7 @@ "version": "1.0", "build": "1", "python_version": "3.X.0", + "console_app": false, "universal_build": true, "host_arch": "arm64", "_extensions": [ diff --git a/{{ cookiecutter.format }}/installer/Distribution.xml b/{{ cookiecutter.format }}/installer/Distribution.xml new file mode 100644 index 0000000..6e21ea1 --- /dev/null +++ b/{{ cookiecutter.format }}/installer/Distribution.xml @@ -0,0 +1,15 @@ + + + {{ cookiecutter.formal_name }} + + + + + + + + + + + {{ cookiecutter.app_name }}.pkg + diff --git a/{{ cookiecutter.format }}/installer/resources/welcome.html b/{{ cookiecutter.format }}/installer/resources/welcome.html new file mode 100644 index 0000000..97100d0 --- /dev/null +++ b/{{ cookiecutter.format }}/installer/resources/welcome.html @@ -0,0 +1,15 @@ + + + + + + + +

{{ cookiecutter.formal_name }} {{ cookiecutter.version }}

+

This installer will guide you through the steps necessary to install {{ cookiecutter.formal_name }}.

+ + diff --git a/{{ cookiecutter.format }}/installer/scripts/postinstall b/{{ cookiecutter.format }}/installer/scripts/postinstall new file mode 100755 index 0000000..12d5afc --- /dev/null +++ b/{{ cookiecutter.format }}/installer/scripts/postinstall @@ -0,0 +1,7 @@ +#!/bin/sh +echo "Post installation process started" + +echo "Install binary symlink" +ln -si "/Library/{{ cookiecutter.formal_name }}/{{ cookiecutter.formal_name }}.app/Contents/MacOS/{{ cookiecutter.formal_name }}" /usr/local/bin/{{ cookiecutter.app_name }} + +echo "Post installation process finished" diff --git a/{{ cookiecutter.format }}/{{ cookiecutter.class_name }}/main.m b/{{ cookiecutter.format }}/{{ cookiecutter.class_name }}/main.m index b15f3ed..a250b27 100644 --- a/{{ cookiecutter.format }}/{{ cookiecutter.class_name }}/main.m +++ b/{{ cookiecutter.format }}/{{ cookiecutter.class_name }}/main.m @@ -7,23 +7,37 @@ #import #include #include - - +#include +#include + +// A global indicator +char *debug_mode; + +NSString * format_traceback(PyObject *, PyObject *, PyObject *); +{% if cookiecutter.console_app %} +void info_log(NSString *format, ...); +void debug_log(NSString *format, ...); +{% else %} +#define info_log(...) NSLog(__VA_ARGS__) +#define debug_log(...) if (debug_mode) NSLog(__VA_ARGS__) +{% endif %} +NSBundle *get_main_bundle(void); +void setup_stdout(NSBundle *); void crash_dialog(NSString *); -NSString * format_traceback(PyObject *type, PyObject *value, PyObject *traceback); int main(int argc, char *argv[]) { int ret = 0; PyStatus status; PyPreConfig preconfig; PyConfig config; + NSBundle *mainBundle; + NSString *resourcePath; NSString *python_home; NSString *app_module_name; NSString *path; NSString *traceback_str; wchar_t *wtmp_str; const char *app_module_str; - const char* nslog_script; PyObject *app_module; PyObject *module; PyObject *module_attr; @@ -35,10 +49,15 @@ int main(int argc, char *argv[]) { PyObject *systemExit_code; @autoreleasepool { - NSString * resourcePath = [[NSBundle mainBundle] resourcePath]; + // Set the global debug state based on the runtime environment + debug_mode = getenv("BRIEFCASE_DEBUG"); + + // Set the resource path for the app + mainBundle = get_main_bundle(); + resourcePath = [mainBundle resourcePath]; // Generate an isolated Python configuration. - NSLog(@"Configuring isolated Python..."); + debug_log(@"Configuring isolated Python..."); PyPreConfig_InitIsolatedConfig(&preconfig); PyConfig_InitIsolatedConfig(&config); @@ -54,7 +73,7 @@ int main(int argc, char *argv[]) { // Isolated apps need to set the full PYTHONPATH manually. config.module_search_paths_set = 1; - NSLog(@"Pre-initializing Python runtime..."); + debug_log(@"Pre-initializing Python runtime..."); status = Py_PreInitialize(&preconfig); if (PyStatus_Exception(status)) { crash_dialog([NSString stringWithFormat:@"Unable to pre-initialize Python interpreter: %s", status.err_msg, nil]); @@ -64,7 +83,7 @@ int main(int argc, char *argv[]) { // Set the home for the Python interpreter python_home = [NSString stringWithFormat:@"%@/support/python-stdlib", resourcePath, nil]; - NSLog(@"PythonHome: %@", python_home); + debug_log(@"PythonHome: %@", python_home); wtmp_str = Py_DecodeLocale([python_home UTF8String], NULL); status = PyConfig_SetString(&config, &config.home, wtmp_str); if (PyStatus_Exception(status)) { @@ -82,9 +101,9 @@ int main(int argc, char *argv[]) { if (app_module_str) { app_module_name = [[NSString alloc] initWithUTF8String:app_module_str]; } else { - app_module_name = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"MainModule"]; + app_module_name = [mainBundle objectForInfoDictionaryKey:@"MainModule"]; if (app_module_name == NULL) { - NSLog(@"Unable to identify app module name."); + debug_log(@"Unable to identify app module name."); } app_module_str = [app_module_name UTF8String]; } @@ -104,11 +123,11 @@ int main(int argc, char *argv[]) { } // Set the full module path. This includes the stdlib, site-packages, and app code. - NSLog(@"PYTHONPATH:"); + debug_log(@"PYTHONPATH:"); // The .zip form of the stdlib path = [NSString stringWithFormat:@"%@/support/python{{ cookiecutter.python_version|py_libtag }}.zip", resourcePath, nil]; - NSLog(@"- %@", path); + debug_log(@"- %@", path); wtmp_str = Py_DecodeLocale([path UTF8String], NULL); status = PyWideStringList_Append(&config.module_search_paths, wtmp_str); if (PyStatus_Exception(status)) { @@ -120,7 +139,7 @@ int main(int argc, char *argv[]) { // The unpacked form of the stdlib path = [NSString stringWithFormat:@"%@/support/python-stdlib", resourcePath, nil]; - NSLog(@"- %@", path); + debug_log(@"- %@", path); wtmp_str = Py_DecodeLocale([path UTF8String], NULL); status = PyWideStringList_Append(&config.module_search_paths, wtmp_str); if (PyStatus_Exception(status)) { @@ -132,7 +151,7 @@ int main(int argc, char *argv[]) { // Add the stdlib binary modules path path = [NSString stringWithFormat:@"%@/support/python-stdlib/lib-dynload", resourcePath, nil]; - NSLog(@"- %@", path); + debug_log(@"- %@", path); wtmp_str = Py_DecodeLocale([path UTF8String], NULL); status = PyWideStringList_Append(&config.module_search_paths, wtmp_str); if (PyStatus_Exception(status)) { @@ -144,7 +163,7 @@ int main(int argc, char *argv[]) { // Add the app_packages path path = [NSString stringWithFormat:@"%@/app_packages", resourcePath, nil]; - NSLog(@"- %@", path); + debug_log(@"- %@", path); wtmp_str = Py_DecodeLocale([path UTF8String], NULL); status = PyWideStringList_Append(&config.module_search_paths, wtmp_str); if (PyStatus_Exception(status)) { @@ -156,7 +175,7 @@ int main(int argc, char *argv[]) { // Add the app path path = [NSString stringWithFormat:@"%@/app", resourcePath, nil]; - NSLog(@"- %@", path); + debug_log(@"- %@", path); wtmp_str = Py_DecodeLocale([path UTF8String], NULL); status = PyWideStringList_Append(&config.module_search_paths, wtmp_str); if (PyStatus_Exception(status)) { @@ -166,7 +185,7 @@ int main(int argc, char *argv[]) { } PyMem_RawFree(wtmp_str); - NSLog(@"Configure argc/argv..."); + debug_log(@"Configure argc/argv..."); status = PyConfig_SetBytesArgv(&config, argc, argv); if (PyStatus_Exception(status)) { crash_dialog([NSString stringWithFormat:@"Unable to configured argc/argv: %s", status.err_msg, nil]); @@ -174,7 +193,7 @@ int main(int argc, char *argv[]) { Py_ExitStatusException(status); } - NSLog(@"Initializing Python runtime..."); + debug_log(@"Initializing Python runtime..."); status = Py_InitializeFromConfig(&config); if (PyStatus_Exception(status)) { crash_dialog([NSString stringWithFormat:@"Unable to initialize Python interpreter: %s", status.err_msg, nil]); @@ -183,30 +202,8 @@ int main(int argc, char *argv[]) { } @try { - // Install the nslog script to redirect stdout/stderr if available. - // Set the name of the python NSLog bootstrap script - nslog_script = [ - [[NSBundle mainBundle] pathForResource:@"app_packages/nslog" - ofType:@"py"] cStringUsingEncoding:NSUTF8StringEncoding]; - - if (nslog_script == NULL) { - NSLog(@"No Python NSLog handler found. stdout/stderr will not be captured."); - NSLog(@"To capture stdout/stderr, add 'std-nslog' to your app dependencies."); - } else { - NSLog(@"Installing Python NSLog handler..."); - FILE *fd = fopen(nslog_script, "r"); - if (fd == NULL) { - crash_dialog(@"Unable to open nslog.py"); - exit(-1); - } - - ret = PyRun_SimpleFileEx(fd, nslog_script, 1); - fclose(fd); - if (ret != 0) { - crash_dialog(@"Unable to install Python NSLog handler"); - exit(ret); - } - } + // Set up an stdout/stderr handling that is required + setup_stdout(mainBundle); // Start the app module. // @@ -215,7 +212,7 @@ int main(int argc, char *argv[]) { // pymain_run_module() method); we need to re-implement it // because we need to be able to inspect the error state of // the interpreter, not just the return code of the module. - NSLog(@"Running app module: %@", app_module_name); + debug_log(@"Running app module: %@", app_module_name); module = PyImport_ImportModule("runpy"); if (module == NULL) { crash_dialog(@"Could not import runpy module"); @@ -241,7 +238,7 @@ int main(int argc, char *argv[]) { } // Print a separator to differentiate Python startup logs from app logs - NSLog(@"---------------------------------------------------------------------------"); + debug_log(@"---------------------------------------------------------------------------"); // Invoke the app module result = PyObject_Call(module_attr, method_args, NULL); @@ -259,31 +256,21 @@ int main(int argc, char *argv[]) { if (PyErr_GivenExceptionMatches(exc_value, PyExc_SystemExit)) { systemExit_code = PyObject_GetAttrString(exc_value, "code"); if (systemExit_code == NULL) { - NSLog(@"Could not determine exit code"); + debug_log(@"Could not determine exit code"); ret = -10; } else { ret = (int) PyLong_AsLong(systemExit_code); } } else { + // Non-SystemExit; likely an uncaught exception ret = -6; - } - - if (ret != 0) { - NSLog(@"Application quit abnormally (Exit code %d)!", ret); - - traceback_str = format_traceback(exc_type, exc_value, exc_traceback); - - // Restore the error state of the interpreter. - PyErr_Restore(exc_type, exc_value, exc_traceback); - - // Print exception to stderr. - // In case of SystemExit, this will call exit() - PyErr_Print(); + info_log(@"---------------------------------------------------------------------------"); + info_log(@"Application quit abnormally!"); // Display stack trace in the crash dialog. + traceback_str = format_traceback(exc_type, exc_value, exc_traceback); crash_dialog(traceback_str); - exit(ret); } } } @@ -300,52 +287,6 @@ int main(int argc, char *argv[]) { return ret; } - -/** - * Construct and display a modal dialog to the user that contains - * details of an error during application execution (usually a traceback). - */ -void crash_dialog(NSString *details) { - // Write the error to the log - NSLog(@"%@", details); - - // If there's an app module override, we're running in test mode; don't show error dialogs - if (getenv("BRIEFCASE_MAIN_MODULE")) { - return; - } - - // Obtain the app instance (starting it if necessary) so that we can show an error dialog - NSApplication *app = [NSApplication sharedApplication]; - [app setActivationPolicy:NSApplicationActivationPolicyRegular]; - - // Create a stack trace dialog - NSAlert *alert = [[NSAlert alloc] init]; - [alert setAlertStyle:NSAlertStyleCritical]; - [alert setMessageText:@"Application has crashed"]; - [alert setInformativeText:@"An unexpected error occurred. Please see the traceback below for more information."]; - - // A multiline text widget in a scroll view to contain the stack trace - NSScrollView *scroll_panel = [[NSScrollView alloc] initWithFrame:NSMakeRect(0, 0, 600, 300)]; - [scroll_panel setHasVerticalScroller:true]; - [scroll_panel setHasHorizontalScroller:false]; - [scroll_panel setAutohidesScrollers:false]; - [scroll_panel setBorderType:NSBezelBorder]; - - NSTextView *crash_text = [[NSTextView alloc] init]; - [crash_text setEditable:false]; - [crash_text setSelectable:true]; - [crash_text setString:details]; - [crash_text setVerticallyResizable:true]; - [crash_text setHorizontallyResizable:true]; - [crash_text setFont:[NSFont fontWithName:@"Menlo" size:12.0]]; - - [scroll_panel setDocumentView:crash_text]; - [alert setAccessoryView:scroll_panel]; - - // Show the crash dialog - [alert runModal]; -} - /** * Convert a Python traceback object into a user-suitable string, stripping off * stack context that comes from this stub binary. @@ -374,7 +315,6 @@ void crash_dialog(NSString *details) { // Format the traceback. traceback_module = PyImport_ImportModule("traceback"); if (traceback_module == NULL) { - NSLog(@"Could not import traceback"); return @"Could not import traceback"; } @@ -382,11 +322,9 @@ void crash_dialog(NSString *details) { if (format_exception && PyCallable_Check(format_exception)) { traceback_list = PyObject_CallFunctionObjArgs(format_exception, type, value, traceback, NULL); } else { - NSLog(@"Could not find 'format_exception' in 'traceback' module"); return @"Could not find 'format_exception' in 'traceback' module"; } if (traceback_list == NULL) { - NSLog(@"Could not format traceback"); return @"Could not format traceback"; } @@ -404,3 +342,137 @@ void crash_dialog(NSString *details) { withTemplate:@" File \"$1.app/Contents/Resources/"]; return traceback_str; } + +{% if cookiecutter.console_app %} +void info_log(NSString *format, ...) { + va_list args; + va_start(args, format); + printf("%s\n", [[[NSString alloc] initWithFormat:format arguments:args] UTF8String]); + va_end(args); +} + +void debug_log(NSString *format, ...) { + if (debug_mode) { + va_list args; + va_start(args, format); + printf("%s\n", [[[NSString alloc] initWithFormat:format arguments:args] UTF8String]); + va_end(args); + } +} + +/**************************************************************************** + * In a normal macOS app, [NSBundle mainBundle] works as expected. However, + * the path it generates is based on sys.argv[0], which won't be the same if + * you symlink to the binary to expose a command line app. Instead, use + * _NSGetExecutablePath to get the binary path, then construct the bundle + * path based on the known file structure of the app bundle. + ****************************************************************************/ +NSBundle* get_main_bundle(void) { + uint32_t path_max = PATH_MAX; + char binary_path[PATH_MAX]; + char resolved_binary_path[PATH_MAX]; + char *bundle_path; + NSBundle *mainBundle; + + _NSGetExecutablePath(binary_path, &path_max); + realpath(binary_path, resolved_binary_path); + debug_log(@"Binary: %s", resolved_binary_path); + bundle_path = dirname(dirname(dirname(resolved_binary_path))); + mainBundle = [NSBundle bundleWithPath:[NSString stringWithCString:bundle_path encoding:NSUTF8StringEncoding]]; + debug_log(@"App Bundle: %@", mainBundle); + + return mainBundle; +} + +void setup_stdout(NSBundle *mainBundle) { +} + +void crash_dialog(NSString *details) { + info_log(details); +} + +{% else %} + +NSBundle* get_main_bundle(void) { + return [NSBundle mainBundle]; +} + +void setup_stdout(NSBundle *mainBundle) { + int ret = 0; + const char *nslog_script; + + // Install the nslog script to redirect stdout/stderr if available. + // Set the name of the python NSLog bootstrap script + nslog_script = [ + [mainBundle pathForResource:@"app_packages/nslog" + ofType:@"py"] cStringUsingEncoding:NSUTF8StringEncoding]; + + if (nslog_script == NULL) { + info_log(@"No Python NSLog handler found. stdout/stderr will not be captured."); + info_log(@"To capture stdout/stderr, add 'std-nslog' to your app dependencies."); + } else { + debug_log(@"Installing Python NSLog handler..."); + FILE *fd = fopen(nslog_script, "r"); + if (fd == NULL) { + crash_dialog(@"Unable to open nslog.py"); + exit(-1); + } + + ret = PyRun_SimpleFileEx(fd, nslog_script, 1); + fclose(fd); + if (ret != 0) { + crash_dialog(@"Unable to install Python NSLog handler"); + exit(ret); + } + } +} + +/** + * Construct and display a modal dialog to the user that contains + * details of an error during application execution (usually a traceback). + */ +void crash_dialog(NSString *details) { + // Write the error to the log + NSArray *lines = [details componentsSeparatedByString:@"\n"]; + for (int i = 0; i < [lines count]; i++) { + NSLog(@"%@", lines[i]); + } + + // If there's an app module override, we're running in test mode; don't show error dialogs + if (getenv("BRIEFCASE_MAIN_MODULE")) { + return; + } + + // Obtain the app instance (starting it if necessary) so that we can show an error dialog + NSApplication *app = [NSApplication sharedApplication]; + [app setActivationPolicy:NSApplicationActivationPolicyRegular]; + + // Create a stack trace dialog + NSAlert *alert = [[NSAlert alloc] init]; + [alert setAlertStyle:NSAlertStyleCritical]; + [alert setMessageText:@"Application has crashed"]; + [alert setInformativeText:@"An unexpected error occurred. Please see the traceback below for more information."]; + + // A multiline text widget in a scroll view to contain the stack trace + NSScrollView *scroll_panel = [[NSScrollView alloc] initWithFrame:NSMakeRect(0, 0, 600, 300)]; + [scroll_panel setHasVerticalScroller:true]; + [scroll_panel setHasHorizontalScroller:false]; + [scroll_panel setAutohidesScrollers:false]; + [scroll_panel setBorderType:NSBezelBorder]; + + NSTextView *crash_text = [[NSTextView alloc] init]; + [crash_text setEditable:false]; + [crash_text setSelectable:true]; + [crash_text setString:details]; + [crash_text setVerticallyResizable:true]; + [crash_text setHorizontallyResizable:true]; + [crash_text setFont:[NSFont fontWithName:@"Menlo" size:12.0]]; + + [scroll_panel setDocumentView:crash_text]; + [alert setAccessoryView:scroll_panel]; + + // Show the crash dialog + [alert runModal]; +} + +{% endif %}