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 %}